import { expect, test } from "@playwright/test"; import { KIND_HUDDLE_ENDED, KIND_HUDDLE_STARTED, KIND_TYPING_INDICATOR, } from "../../src/shared/constants/kinds"; import { TEST_IDENTITIES, installMockBridge, openChannelBrowser, openCreateChannelDialog, openNewMessagePage, } from "../helpers/bridge"; const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"; const AGENTS_CHANNEL_ID = "94a444a4-c0a3-5966-ab05-530c6ddc2301"; const MOCK_IDENTITY_PUBKEY = "deadbeef".repeat(8); const CACHED_PROFILE_LABELS_TAG = "@cached-profile-labels"; // Relay-only agent owned by the mock viewer (see e2eBridge.ts // OWNED_RELAY_AGENT_PUBKEY). Classified as a bot via mockRelayAgents and // owned-by-viewer via its mockProfiles owner_pubkey, so the sidebar // view-activity gate (memberIsBot && viewerIsOwner) opens for it. const OWNED_RELAY_AGENT_PUBKEY = "a1b2c3d4e5f60718293a4b5c6d7e8f90112233445566778899aabbccddeeff00"; const DM_RELAY_AGENT_PUBKEY = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; type MockFeedWindow = Window & { __BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: { channelName: string; content: string; createdAt?: number; id?: string; parentEventId?: string; pubkey?: string; }) => { content: string; created_at: number; id: string; kind: number; pubkey: string; tags: string[][]; }; __BUZZ_E2E_SEED_ACTIVE_TURNS__?: (input: { agentPubkey: string; channelId: string; turnId: string; kind?: "turn_started" | "turn_completed"; }) => void; __BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?: (item: { category: "mention" | "needs_action" | "activity" | "agent_activity"; channel_id: string | null; channel_name: string; channel_type?: string | null; content: string; created_at: number; id: string; kind: number; pubkey: string; tags: string[][]; }) => unknown; }; async function hasOutgoingEventWithContent( page: import("@playwright/test").Page, content: string, ) { return page.evaluate((expectedContent) => { return ( ( window as Window & { __BUZZ_E2E_COMMAND_LOG__?: Array<{ command: string; payload: unknown; }>; } ).__BUZZ_E2E_COMMAND_LOG__?.some((entry) => { if (entry.command !== "plugin:websocket|send") { return false; } const data = ( entry.payload as { message?: { data?: string; type?: string }; } )?.message?.data; if (!data) { return false; } try { const frame = JSON.parse(data) as unknown[]; const event = frame[1] as { content?: unknown } | undefined; return frame[0] === "EVENT" && event?.content === expectedContent; } catch { return false; } }) ?? false ); }, content); } async function readOutgoingChannelId( page: import("@playwright/test").Page, content: string, ) { return page.evaluate((expectedContent) => { const entries = ( window as Window & { __BUZZ_E2E_COMMAND_LOG__?: Array<{ command: string; payload: unknown; }>; } ).__BUZZ_E2E_COMMAND_LOG__ ?? []; for (const entry of entries) { if (entry.command !== "plugin:websocket|send") { continue; } const data = ( entry.payload as { message?: { data?: string } } | undefined )?.message?.data; if (!data) { continue; } try { const frame = JSON.parse(data) as [ string, { content?: string; tags?: string[][] }, ]; if ( frame[0] !== "EVENT" || !frame[1]?.content?.includes(expectedContent) ) { continue; } return frame[1].tags?.find((tag) => tag[0] === "h")?.[1] ?? null; } catch {} } return null; }, content); } async function openChannelManagement( page: import("@playwright/test").Page, channelName: string, ) { await page.getByTestId(`channel-${channelName}`).click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); await page.getByTestId("channel-management-trigger").click(); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); } async function closeChannelManagement(page: import("@playwright/test").Page) { await page.getByTestId("auxiliary-panel-close").click(); await expect(page.getByTestId("channel-management-sheet")).not.toBeVisible(); } async function openChannelEditDialog(page: import("@playwright/test").Page) { await page.getByTestId("channel-management-edit").click(); await expect( page.getByRole("dialog", { name: /Edit (?:public|private) channel/, }), ).toBeVisible(); } async function openMembersSidebar( page: import("@playwright/test").Page, channelName: string, ) { await page.getByTestId(`channel-${channelName}`).click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); await page.getByTestId("channel-members-trigger").click(); await expect(page.getByTestId("members-sidebar")).toBeVisible(); } async function readMembersTriggerCount(page: import("@playwright/test").Page) { const label = (await page .getByTestId("channel-members-trigger") .getAttribute("aria-label")) ?? ""; const match = label.match(/\((\d+)\)$/); if (!match) { throw new Error(`Could not read member count from label: ${label}`); } return Number(match[1]); } async function expectMembersTriggerCount( page: import("@playwright/test").Page, count: number, ) { await expect(page.getByTestId("channel-members-trigger")).toHaveAttribute( "aria-label", `View channel members (${count})`, ); } async function waitForMockLiveSubscription( page: import("@playwright/test").Page, channelName: string, kind?: number, ) { await expect .poll(async () => { return page.evaluate( ({ currentChannelName, kind }) => { return ( ( 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); } async function openMemberMenu( page: import("@playwright/test").Page, pubkey: string, ) { const row = page.getByTestId(`sidebar-member-${pubkey}`); const trigger = page.getByTestId(`sidebar-member-menu-${pubkey}`); await row.scrollIntoViewIfNeeded(); await row.hover(); // Workaround: @radix-ui/react-dropdown-menu@2.1.16 ignores pointer-based // re-opens after a menu item click (onCloseAutoFocus race). Opening via // keyboard (focus + Enter) is reliable. Revisit if Radix fixes this. await trigger.focus(); await trigger.press("Enter"); await expect(trigger).toHaveAttribute("data-state", "open"); } async function addGenericAgent( page: import("@playwright/test").Page, channelName: string, agentName: string, ): Promise { await page.getByTestId(`channel-${channelName}`).click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); const channelId = await page .getByTestId(`channel-${channelName}`) .getAttribute("data-channel-id"); if (!channelId) { throw new Error(`Channel ${channelName} is missing a data-channel-id.`); } await page.waitForFunction(() => { return Boolean( ( window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown; } ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__, ); }); return page.evaluate( async ({ agentName, channelId }) => { const invoke = ( window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: ( command: string, payload?: Record, ) => Promise<{ agent?: { pubkey: string } }>; } ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__; if (!invoke) { throw new Error("Mock bridge is not installed."); } const created = await invoke("create_managed_agent", { input: { name: agentName, spawnAfterCreate: true, systemPrompt: "Watch the channel and help when asked.", }, }); const pubkey = created.agent?.pubkey; if (!pubkey) { throw new Error("Mock managed agent creation did not return a pubkey."); } await invoke("add_channel_members", { channelId, pubkeys: [pubkey], role: "bot", }); await ( window as Window & { __BUZZ_E2E_QUERY_CLIENT__?: { invalidateQueries: () => Promise; }; } ).__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries(); return pubkey; }, { agentName, channelId }, ); } async function readCommandLog(page: import("@playwright/test").Page) { return page.evaluate(() => { return ( (window as Window & { __BUZZ_E2E_COMMANDS__?: string[] }) .__BUZZ_E2E_COMMANDS__ ?? [] ); }); } function commandCount(commands: string[], command: string) { return commands.filter((entry) => entry === command).length; } async function readCommandPayloadLog(page: import("@playwright/test").Page) { return page.evaluate(() => { return ( ( window as Window & { __BUZZ_E2E_COMMAND_LOG__?: Array<{ command: string; payload: unknown; }>; } ).__BUZZ_E2E_COMMAND_LOG__ ?? [] ); }); } async function invokeMockCommand( page: import("@playwright/test").Page, command: string, payload?: Record, ) { await page.waitForFunction(() => { return Boolean( ( window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown; } ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__, ); }); return page.evaluate( async ({ command, payload, }: { command: string; payload?: Record; }) => { const invoke = ( window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: ( command: string, payload?: Record, ) => Promise; } ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__; if (!invoke) { throw new Error("Mock bridge is not installed."); } return invoke(command, payload); }, { command, payload }, ) as Promise; } async function expectSameLeftInset( page: import("@playwright/test").Page, firstTestId: string, secondTestId: string, ) { const firstBox = await page.getByTestId(firstTestId).boundingBox(); const secondBox = await page.getByTestId(secondTestId).first().boundingBox(); if (!firstBox || !secondBox) { throw new Error(`Could not measure ${firstTestId} against ${secondTestId}`); } expect(Math.abs(firstBox.x - secondBox.x)).toBeLessThanOrEqual(4); } async function expectIntroSpacedAboveDayDivider( page: import("@playwright/test").Page, introTestId: string, ) { const introBox = await page.getByTestId(introTestId).boundingBox(); const dividerBox = await page .getByTestId("message-timeline-day-divider") .first() .boundingBox(); const messageBox = await page .getByTestId("message-row") .first() .boundingBox(); if (!introBox || !dividerBox || !messageBox) { throw new Error(`Could not measure timeline spacing for ${introTestId}`); } const gapAboveDivider = dividerBox.y - (introBox.y + introBox.height); const gapBelowDivider = messageBox.y - (dividerBox.y + dividerBox.height); // The intro is a flex sibling above the timeline; the day divider and first // message-row are virtualized items positioned by translateY inside the // scroll container. The intro -> divider gap is the wrapper flex spacing the // layout controls (8px, stable), so guard THAT with a tight band — a layout // regression that collapses or balloons it fails here. The divider -> message // gap is NOT a layout-spacing contract: virtualized rows are positioned // back-to-back (no inter-item gap), so it is ~0 by construction plus // MessageRow avatar/font render jitter, genuinely variable run-to-run. Assert // only non-overlap on it (reading order: intro, divider, then message). expect(Math.abs(gapAboveDivider - 8)).toBeLessThanOrEqual(2); expect(gapBelowDivider).toBeGreaterThanOrEqual(0); } async function expectIntroActionCardLayout( page: import("@playwright/test").Page, actionTestId: string, ) { const actionBox = await page.getByTestId(actionTestId).boundingBox(); const iconBox = await page.getByTestId(`${actionTestId}-icon`).boundingBox(); if (!actionBox || !iconBox) { throw new Error(`Could not measure intro action card: ${actionTestId}`); } expect(actionBox.height).toBeGreaterThan(actionBox.width); expect(Math.round(actionBox.width)).toBe(220); expect(Math.round(iconBox.width)).toBe(48); expect(Math.round(iconBox.height)).toBe(48); const introIconRadius = await page .getByTestId("message-channel-intro-icon") .evaluate((element) => window.getComputedStyle(element).borderRadius); const actionRadius = await page .getByTestId(actionTestId) .evaluate((element) => window.getComputedStyle(element).borderRadius); expect(actionRadius).toBe(introIconRadius); await expect(page.getByTestId(`${actionTestId}-title`)).toHaveCSS( "white-space", "normal", ); await expect(page.getByTestId(`${actionTestId}-description`)).toHaveCSS( "white-space", "normal", ); } async function expectIntroActionsShareRow( page: import("@playwright/test").Page, actionTestIds: string[], ) { const boxes = await Promise.all( actionTestIds.map((testId) => page.getByTestId(testId).boundingBox()), ); const measuredBoxes = boxes.filter( (box): box is NonNullable => box !== null, ); const [firstBox] = measuredBoxes; if (!firstBox || measuredBoxes.length !== actionTestIds.length) { throw new Error("Could not measure intro action row"); } for (const box of measuredBoxes.slice(1)) { expect(Math.abs(firstBox.y - box.y)).toBeLessThanOrEqual(1); } } test.beforeEach(async ({ page }, testInfo) => { await installMockBridge( page, testInfo.tags.includes(CACHED_PROFILE_LABELS_TAG) ? { usersBatchDelayMs: 10_000 } : undefined, ); }); test("sidebar shows all channel types", async ({ page }) => { await page.goto("/"); await expect(page.getByTestId("app-sidebar")).toBeVisible(); await expect(page.getByTestId("sidebar-agents-count")).toHaveCount(0); // Streams const streamList = page.getByTestId("stream-list"); await expect(streamList).toContainText("general"); await expect(streamList).toContainText("random"); await expect(streamList).toContainText("engineering"); await expect(streamList).toContainText("agents"); // Forums const forumList = page.getByTestId("forum-list"); await expect(forumList).toContainText("watercooler"); await expect(forumList).toContainText("announcements"); // DMs const dmList = page.getByTestId("dm-list"); await expect(dmList).toContainText("alice-tyler"); await expect(dmList).toContainText("bob-tyler"); }); test("shows cached profile labels while relay profiles revalidate", { tag: CACHED_PROFILE_LABELS_TAG, }, async ({ page }) => { await page.addInitScript( ({ alicePubkey }) => { window.localStorage.setItem( "buzz-user-labels.v1:ws://localhost:3000", JSON.stringify({ version: 1, updatedAt: Date.now(), profiles: { [alicePubkey]: { displayName: "Cached Alice", name: "alice", nip05Handle: null, }, }, }), ); }, { alicePubkey: TEST_IDENTITIES.alice.pubkey }, ); await page.goto("/"); await page.getByTestId("channel-general").click(); const aliceMessage = page .getByTestId("message-row") .filter({ hasText: "Hey team — checking in." }); await expect(aliceMessage.getByTestId("message-author")).toHaveText( "Cached Alice", { timeout: 1_000 }, ); }); test("shows presence in sidebar, DM header, and member list", async ({ page, }) => { await page.goto("/"); await expect(page.getByTestId("sidebar-profile-card")).toBeVisible(); await expect(page.getByTestId("self-presence-badge")).toHaveAttribute( "aria-label", "Online", ); await expect(page.getByTestId("channel-presence-alice-tyler")).toBeVisible(); await page.getByTestId("channel-alice-tyler").click(); await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); await expect(page.getByTestId("chat-presence-badge")).toContainText("Online"); await openMembersSidebar(page, "general"); await expect( page.getByTestId(`sidebar-member-presence-${TEST_IDENTITIES.alice.pubkey}`), ).toBeVisible(); await expect( page.getByTestId(`sidebar-member-presence-${TEST_IDENTITIES.bob.pubkey}`), ).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByTestId("members-sidebar")).not.toBeVisible(); }); test("start a new direct message from the sidebar", async ({ page }) => { await page.goto("/"); await openNewMessagePage(page); await expect(page.getByTestId("new-message-page")).toBeVisible(); await expect(page.getByTestId("message-composer")).toBeVisible(); await expect(page.getByTestId("new-message-recipient-popover")).toBeVisible(); await expect(page.getByTestId("new-message-body")).toBeEmpty(); await page.getByTestId("new-dm-search").fill("charlie"); await expect( page.getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`), ).toBeVisible(); await page.keyboard.press("Enter"); await expect( page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`), ).toBeVisible(); await expect(page.getByTestId("new-dm-search")).toHaveValue(""); const selectedCharlie = page.getByTestId( `new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`, ); await selectedCharlie.hover(); const selectedCharlieBox = await selectedCharlie.boundingBox(); expect(selectedCharlieBox).not.toBeNull(); if (!selectedCharlieBox) return; await page.mouse.move( selectedCharlieBox.x + selectedCharlieBox.width / 2, selectedCharlieBox.y + selectedCharlieBox.height / 2, ); await page.mouse.down(); await expect(page.locator(".buzz-poof-burst")).toHaveCount(1); await page.mouse.up(); await expect(selectedCharlie).not.toBeVisible(); await page.getByTestId("new-dm-search").fill("charlie"); await expect( page.getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`), ).toBeVisible(); await page .getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); await expect(page.getByTestId("new-dm-search")).toHaveValue(""); await expect( page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`), ).toBeVisible(); await page.getByTestId("message-input").fill("Hello charlie"); await page.getByTestId("send-message").click(); await expect(page.getByTestId("dm-list")).toContainText("charlie"); await expect(page.getByTestId("chat-title")).toHaveText("charlie"); await expect(page.getByTestId("section-actions-dms")).not.toBeFocused(); }); test("keeps typing focus while arrow keys traverse and select DM recipients", async ({ page, }) => { await page.goto("/"); await openNewMessagePage(page); const search = page.getByTestId("new-dm-search"); await expect(search).toBeFocused(); const initialActiveDescendant = await search.getAttribute( "aria-activedescendant", ); expect(initialActiveDescendant).toMatch(/^new-dm-option-/); await search.press("Enter"); await expect( page.locator("button[data-testid^='new-dm-selected-']"), ).toHaveCount(1); await expect(page.getByTestId("new-message-recipient-popover")).toBeVisible(); await search.fill("o"); await expect( page.getByTestId(`new-dm-result-${TEST_IDENTITIES.bob.pubkey}`), ).toBeVisible(); await expect( page.getByTestId(`new-dm-result-${TEST_IDENTITIES.outsider.pubkey}`), ).toBeVisible(); await expect(search).toHaveAttribute( "aria-activedescendant", `new-dm-option-${TEST_IDENTITIES.outsider.pubkey}`, ); await expect( page.locator(`#new-dm-option-${TEST_IDENTITIES.outsider.pubkey}`), ).toHaveAttribute("aria-selected", "true"); await search.press("ArrowDown"); await expect(search).toBeFocused(); await expect(search).toHaveAttribute( "aria-activedescendant", `new-dm-option-${TEST_IDENTITIES.bob.pubkey}`, ); await search.press("ArrowUp"); await expect(search).toHaveAttribute( "aria-activedescendant", `new-dm-option-${TEST_IDENTITIES.outsider.pubkey}`, ); await search.press("Enter"); await expect(search).toBeFocused(); await expect(search).toHaveValue(""); await expect(search).toHaveAttribute("aria-activedescendant", /.+/); await expect( page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.outsider.pubkey}`), ).toBeVisible(); await expect(page.getByTestId("new-message-recipient-popover")).toBeVisible(); }); test("sends the first message from the new direct message composer", async ({ page, }) => { await page.goto("/"); await openNewMessagePage(page); await page.getByTestId("new-dm-search").fill("charlie"); await page .getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); const message = "First message from the new conversation"; await page.getByTestId("message-input").fill(message); await page.getByTestId("send-message").click(); await expect(page.getByTestId("chat-title")).toHaveText("charlie"); await expect(page.getByTestId("message-timeline")).toContainText(message); }); test("creates the DM before preparing a persona mention", async ({ page }) => { await installMockBridge(page, { activePersonaIds: ["builtin:fizz"], createManagedAgentDelayMs: 1_000, }); await page.goto("/"); await openNewMessagePage(page); await page.getByTestId("new-dm-search").fill("charlie"); await page .getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); const input = page.getByTestId("message-input"); await input.fill("Ask @fi"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") .locator("button", { hasText: "Fizz" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" for a hand"); const baselineCommands = await readCommandLog(page); const baselineCommandPayloads = await readCommandPayloadLog(page); const baselineOpenDmCount = commandCount(baselineCommands, "open_dm"); const baselineCreateCount = commandCount( baselineCommands, "create_managed_agent", ); await page.getByTestId("send-message").click(); await expect .poll(async () => commandCount(await readCommandLog(page), "open_dm")) .toBeGreaterThan(baselineOpenDmCount); await expect( page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`), ).toBeDisabled(); await expect(page.getByTestId("new-dm-search")).toBeDisabled(); await expect .poll(async () => commandCount(await readCommandLog(page), "create_managed_agent"), ) .toBeGreaterThan(baselineCreateCount); await expect(page.getByTestId("chat-title")).toContainText("charlie"); await expect(page.getByTestId("chat-title")).toContainText("Fizz"); // Assert popover hidden after chat-title settles — by this point the send // flow has completed and the UI has fully transitioned away from the popover. await expect(page.getByTestId("new-message-recipient-popover")).toBeHidden(); const sendCommands = (await readCommandLog(page)).slice( baselineCommands.length, ); const sendCommandPayloads = (await readCommandPayloadLog(page)).slice( baselineCommandPayloads.length, ); expect(commandCount(sendCommands, "open_dm")).toBe(2); const firstOpenIndex = sendCommands.indexOf("open_dm"); const createIndex = sendCommands.indexOf("create_managed_agent"); const expandedOpenIndex = sendCommands.lastIndexOf("open_dm"); const startIndex = sendCommands.indexOf("start_managed_agent"); expect(firstOpenIndex).toBeLessThan(createIndex); expect(createIndex).toBeLessThan(expandedOpenIndex); expect(expandedOpenIndex).toBeLessThan(startIndex); expect(sendCommands).not.toContain("add_channel_members"); const sentMessageCommand = sendCommandPayloads.find((entry) => { if (entry.command !== "plugin:websocket|send") { return false; } const data = (entry.payload as { message?: { data?: string } } | undefined) ?.message?.data; if (!data) { return false; } const frame = JSON.parse(data) as unknown[]; return ( frame[0] === "EVENT" && (frame[1] as { content?: string } | undefined)?.content.includes( "for a hand", ) ); }); const sentMessageData = ( sentMessageCommand?.payload as { message?: { data?: string } } | undefined )?.message?.data; expect(sentMessageData).toBeTruthy(); const sentMessageEvent = ( JSON.parse(sentMessageData ?? "[]") as [string, { tags?: string[][] }] )[1]; const sentChannelId = sentMessageEvent.tags?.find( (tag) => tag[0] === "h", )?.[1]; expect(sentChannelId).toBeTruthy(); await expect( page.locator("[data-active='true'][data-channel-id]"), ).toHaveAttribute("data-channel-id", sentChannelId ?? ""); await expect(page.getByTestId("message-timeline")).toContainText( "for a hand", ); await expect( page .getByTestId("message-row") .last() .locator("[data-mention].agent-mention-highlight", { hasText: "Fizz" }), ).toBeVisible(); }); test("routes an agent mention from an existing DM to the expanded conversation", async ({ page, }) => { // Delay persona provisioning so the follow-up expanded-DM open/start sequence // cannot collapse into the same fast CI tick before assertions observe it. await installMockBridge(page, { activePersonaIds: ["builtin:fizz"], createManagedAgentDelayMs: 100, }); await page.goto("/"); const sourceDm = page.getByTestId("channel-alice-tyler"); const sourceDmId = await sourceDm.getAttribute("data-channel-id"); expect(sourceDmId).toBeTruthy(); await sourceDm.click(); await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); const messageTail = "in this DM"; const input = page.getByTestId("message-input"); await input.fill("Ask @fi"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") .locator("button", { hasText: "Fizz" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" in this DM"); const baselineCommands = await readCommandPayloadLog(page); await page.getByTestId("send-message").click(); await expect .poll(async () => readOutgoingChannelId(page, messageTail)) .not.toBeNull(); const sentChannelId = await readOutgoingChannelId(page, messageTail); expect(sentChannelId).not.toBe(sourceDmId); await expect( page.locator("[data-active='true'][data-channel-id]"), ).toHaveAttribute("data-channel-id", sentChannelId ?? ""); await expect(page.getByTestId("chat-title")).toContainText("alice"); await expect(page.getByTestId("chat-title")).toContainText("Fizz"); await expect(sourceDm).not.toContainText("Fizz"); const sendCommands = (await readCommandPayloadLog(page)).slice( baselineCommands.length, ); expect(sendCommands).not.toEqual( expect.arrayContaining([ expect.objectContaining({ command: "add_channel_members", payload: expect.objectContaining({ channelId: sourceDmId }), }), ]), ); expect(sendCommands.map((entry) => entry.command)).toEqual( expect.arrayContaining(["open_dm", "start_managed_agent"]), ); }); test("routes a managed relay-agent mention from an existing DM to the expanded conversation", async ({ page, }) => { // Delay the expanded open_dm call so routing/navigation settles // deterministically under fast CI execution. await installMockBridge(page, { openDmDelayMs: 100, managedAgents: [ { pubkey: DM_RELAY_AGENT_PUBKEY, name: "quinn", status: "stopped", }, ], relayAgents: [ { pubkey: DM_RELAY_AGENT_PUBKEY, name: "quinn", respondTo: "allowlist", respondToAllowlist: [MOCK_IDENTITY_PUBKEY], }, ], }); await page.goto("/"); const sourceDm = page.getByTestId("channel-alice-tyler"); const sourceDmId = await sourceDm.getAttribute("data-channel-id"); expect(sourceDmId).toBeTruthy(); await sourceDm.click(); await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); const messageTail = "with the relay agent"; const input = page.getByTestId("message-input"); await input.fill("Ask @qui"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") .locator("button", { hasText: "quinn" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(` ${messageTail}`); const baselineCommands = await readCommandPayloadLog(page); await page.getByTestId("send-message").click(); await expect .poll(async () => readOutgoingChannelId(page, messageTail)) .not.toBeNull(); const sentChannelId = await readOutgoingChannelId(page, messageTail); expect(sentChannelId).not.toBe(sourceDmId); await expect( page.locator("[data-active='true'][data-channel-id]"), ).toHaveAttribute("data-channel-id", sentChannelId ?? ""); await expect(page.getByTestId("chat-title")).toContainText("alice"); await expect(page.getByTestId("chat-title")).toContainText("quinn"); const sendCommands = (await readCommandPayloadLog(page)).slice( baselineCommands.length, ); expect(sendCommands.map((entry) => entry.command)).toContain("open_dm"); expect(sendCommands.map((entry) => entry.command)).toContain( "start_managed_agent", ); expect(sendCommands.map((entry) => entry.command)).not.toContain( "add_channel_members", ); }); test("does not reroute an expanded DM after the user navigates away", async ({ page, }) => { await installMockBridge(page, { activePersonaIds: ["builtin:fizz"], sendMessageDelayMs: 1_000, }); await page.goto("/"); await page.getByTestId("channel-alice-tyler").click(); await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); const input = page.getByTestId("message-input"); await input.fill("Ask @fi"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") .locator("button", { hasText: "Fizz" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" while I leave"); await page.getByTestId("send-message").click(); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.waitForTimeout(1_250); await expect(page).toHaveURL( new RegExp(`/channels/${GENERAL_CHANNEL_ID}(?:\\?|$)`), ); await expect(page.getByTestId("chat-title")).toHaveText("general"); }); test("does not reroute an expanded DM after the channel pane unmounts", async ({ page, }) => { await installMockBridge(page, { activePersonaIds: ["builtin:fizz"], openDmDelayMs: 1_000, }); await page.goto("/"); await page.getByTestId("channel-alice-tyler").click(); await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); const input = page.getByTestId("message-input"); await input.fill("Ask @fi"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") .locator("button", { hasText: "Fizz" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" while I open settings"); await page.getByTestId("send-message").click(); await page.getByTestId("open-settings").click(); await page.getByTestId("profile-popover-settings").click(); await expect(page.getByTestId("settings-view")).toBeVisible(); await page.waitForTimeout(1_250); await expect(page.getByTestId("settings-view")).toBeVisible(); await expect .poll(async () => readOutgoingChannelId(page, "while I open settings")) .toBeNull(); }); test("drops an expanded DM after the first message fails", async ({ page }) => { const retryMessage = "Retry without the agent"; const sendError = "Mock first DM send failed."; // Delay persona provisioning so the follow-up expanded-DM open/start sequence // cannot collapse into the same fast CI tick before assertions observe it. await installMockBridge(page, { activePersonaIds: ["builtin:fizz"], createManagedAgentDelayMs: 100, sendMessageErrors: [sendError], }); await page.goto("/"); await openNewMessagePage(page); await page.getByTestId("new-dm-search").fill("charlie"); await page .getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); const input = page.getByTestId("message-input"); await input.fill("Ask @fi"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") .locator("button", { hasText: "Fizz" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" for a hand"); await page.getByTestId("send-message").click(); await expect(page.getByText(sendError)).toBeVisible(); await expect(input).toContainText("Fizz"); const commandsAfterFailure = await readCommandPayloadLog(page); const failedSendChannelId = await readOutgoingChannelId(page, "for a hand"); expect(failedSendChannelId).toBeTruthy(); expect(commandsAfterFailure.map((entry) => entry.command)).not.toContain( "add_channel_members", ); const baselineOpenDmCount = commandCount( commandsAfterFailure.map((entry) => entry.command), "open_dm", ); await input.fill(retryMessage); const retryBaseline = commandsAfterFailure.length; await page.getByTestId("send-message").click(); await expect(page.getByTestId("chat-title")).toHaveText("charlie"); await expect(page.getByTestId("message-timeline")).toContainText( retryMessage, ); const allCommands = await readCommandPayloadLog(page); expect( commandCount( allCommands.map((entry) => entry.command), "open_dm", ), ).toBe(baselineOpenDmCount + 1); const retryCommands = allCommands.slice(retryBaseline); const retrySend = retryCommands.find((entry) => { if (entry.command !== "plugin:websocket|send") { return false; } const data = (entry.payload as { message?: { data?: string } } | undefined) ?.message?.data; if (!data) { return false; } const frame = JSON.parse(data) as unknown[]; return ( frame[0] === "EVENT" && (frame[1] as { content?: string } | undefined)?.content === retryMessage ); }); const retrySendData = ( retrySend?.payload as { message?: { data?: string } } | undefined )?.message?.data; expect(retrySendData).toBeTruthy(); const retryEvent = ( JSON.parse(retrySendData ?? "[]") as [string, { tags?: string[][] }] )[1]; const retryChannelId = retryEvent.tags?.find((tag) => tag[0] === "h")?.[1]; expect(retryChannelId).toBeTruthy(); expect(retryChannelId).not.toBe(failedSendChannelId); await expect( page.locator("[data-active='true'][data-channel-id]"), ).toHaveAttribute("data-channel-id", retryChannelId ?? ""); }); test("drops an expanded DM after agent startup fails", async ({ page }) => { const retryMessage = "Retry after agent startup failed"; const startError = "Mock agent startup failed."; await installMockBridge(page, { activePersonaIds: ["builtin:fizz"], startManagedAgentErrors: [startError], }); await page.goto("/"); await openNewMessagePage(page); await page.getByTestId("new-dm-search").fill("charlie"); await page .getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); const input = page.getByTestId("message-input"); await input.fill("Ask @fi"); await expect( page .getByTestId("message-composer") .getByTestId("mention-autocomplete") .locator("button", { hasText: "Fizz" }), ).toBeVisible(); await input.press("Enter"); await page.keyboard.type(" before startup fails"); await page.getByTestId("send-message").click(); await expect( page.getByText(startError, { exact: false }).first(), ).toBeVisible(); await expect(input).toContainText("Fizz"); const commandsAfterFailure = await readCommandPayloadLog(page); const openDmCallsAfterFailure = commandsAfterFailure.filter( (entry) => entry.command === "open_dm", ); expect(openDmCallsAfterFailure).toHaveLength(2); expect( (openDmCallsAfterFailure.at(-1)?.payload as { pubkeys?: string[] }) ?.pubkeys, ).toEqual(expect.arrayContaining([TEST_IDENTITIES.charlie.pubkey])); expect( (openDmCallsAfterFailure.at(-1)?.payload as { pubkeys?: string[] }) ?.pubkeys, ).toHaveLength(2); await input.fill(retryMessage); const retryBaseline = commandsAfterFailure.length; await page.getByTestId("send-message").click(); await expect(page.getByTestId("chat-title")).toHaveText("charlie"); await expect(page.getByTestId("message-timeline")).toContainText( retryMessage, ); const retryCommands = (await readCommandPayloadLog(page)).slice( retryBaseline, ); const retryOpenDm = retryCommands.find( (entry) => entry.command === "open_dm", ); expect( (retryOpenDm?.payload as { pubkeys?: string[] } | undefined)?.pubkeys, ).toEqual([TEST_IDENTITIES.charlie.pubkey]); await expect(page.getByTestId("chat-title")).not.toContainText("Fizz"); }); test("closes direct message results while opening", async ({ page }) => { await page.goto("/"); await page.evaluate(() => { const testWindow = window as Window & { __BUZZ_E2E__?: { mock?: { openDmDelayMs?: number } }; }; testWindow.__BUZZ_E2E__ ??= {}; testWindow.__BUZZ_E2E__.mock ??= {}; testWindow.__BUZZ_E2E__.mock.openDmDelayMs = 1_000; }); await openNewMessagePage(page); await page.getByTestId("new-dm-search").fill("charlie"); await page .getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); await expect( page.getByTestId(`new-dm-selected-${TEST_IDENTITIES.charlie.pubkey}`), ).toBeVisible(); await expect(page.getByTestId("new-message-recipient-popover")).toBeVisible(); await page.getByTestId("message-input").fill("Open this conversation"); await page.getByTestId("send-message").click(); await expect(page.getByTestId("new-dm-opening")).toContainText("Opening"); await expect(page.getByTestId("new-message-recipient-popover")).toBeHidden(); await expect(page.getByTestId("chat-title")).toHaveText("charlie"); }); test("does not reopen a direct message after leaving the composer", async ({ page, }) => { await installMockBridge(page, { openDmDelayMs: 1_000 }); await page.goto("/"); await openNewMessagePage(page); await page.getByTestId("new-dm-search").fill("charlie"); await page .getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); const staleMessage = "Do not send after leaving"; await page.getByTestId("message-input").fill(staleMessage); await page.getByTestId("send-message").click(); await expect(page.getByTestId("new-dm-opening")).toContainText("Opening"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(page.getByTestId("dm-list")).toContainText("charlie"); // The DM appearing proves the delayed open completed. Give the abandoned // continuation time to run before asserting that it did not publish. await page.waitForTimeout(1_250); expect(await hasOutgoingEventWithContent(page, staleMessage)).toBe(false); await expect(page).toHaveURL( new RegExp(`/channels/${GENERAL_CHANNEL_ID}(?:\\?|$)`), ); await expect(page.getByTestId("chat-title")).toHaveText("general"); }); test("does not reopen a sent direct message after leaving during cache reseed", async ({ page, }) => { await page.goto("/"); await openNewMessagePage(page); await page.getByTestId("new-dm-search").fill("charlie"); await page .getByTestId(`new-dm-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); const staleMessage = "Stay on the channel after cache reseed"; await page.getByTestId("message-input").fill(staleMessage); await page.evaluate(() => { const testWindow = window as Window & { __BUZZ_E2E__?: { mock?: { channelsReadDelayMs?: number } }; }; testWindow.__BUZZ_E2E__ ??= {}; testWindow.__BUZZ_E2E__.mock ??= {}; testWindow.__BUZZ_E2E__.mock.channelsReadDelayMs = 1_000; }); await page.getByTestId("send-message").click(); await expect .poll(async () => hasOutgoingEventWithContent(page, staleMessage)) .toBe(true); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.waitForTimeout(1_250); await expect(page).toHaveURL( new RegExp(`/channels/${GENERAL_CHANNEL_ID}(?:\\?|$)`), ); await expect(page.getByTestId("chat-title")).toHaveText("general"); }); test("shows capped participant stack in group direct message header", async ({ page, }) => { await page.goto("/"); await openNewMessagePage(page); await expect(page.getByTestId("new-message-page")).toBeVisible(); for (const identity of [ TEST_IDENTITIES.alice, TEST_IDENTITIES.bob, TEST_IDENTITIES.charlie, TEST_IDENTITIES.outsider, ]) { await page.getByTestId("new-dm-search").fill(identity.username); await page.getByTestId(`new-dm-result-${identity.pubkey}`).click(); } await page.getByTestId("message-input").fill("Hello group"); await page.getByTestId("send-message").click(); await expect(page.getByTestId("channel-dm-count-Group DM (5)")).toHaveText( "4", ); await expect(page.getByTestId("chat-title")).toContainText("alice"); await expect(page.getByTestId("chat-title")).toContainText("bob"); await expect(page.getByTestId("chat-title")).toContainText("charlie"); await expect(page.getByTestId("chat-title")).toContainText("+1 more"); await expect(page.getByTestId("chat-title")).not.toContainText("outsider"); const chatTitle = await page.getByTestId("chat-title").innerText(); await expect( page.getByTestId("message-input").locator("[data-placeholder]").first(), ).toHaveAttribute("data-placeholder", `Message ${chatTitle}`); const composerColors = await page .getByTestId("message-input") .evaluate((element) => { const placeholderElement = element.querySelector("[data-placeholder]"); if (!placeholderElement) { return null; } return { placeholderColor: window.getComputedStyle( placeholderElement, "::before", ).color, textColor: window.getComputedStyle(element).color, }; }); expect(composerColors).not.toBeNull(); expect(composerColors?.placeholderColor).not.toBe(composerColors?.textColor); await expect(page.getByTestId("chat-header-dm-avatar")).toHaveCount(0); await expect(page.getByTestId("chat-header-dm-avatar-stack")).toBeVisible(); await expect(page.getByTestId("chat-presence-badge")).toHaveCount(0); await expect( page.getByTestId("chat-header-dm-avatar-stack-participant"), ).toHaveCount(3); await expect(page.getByTestId("chat-header-dm-avatar-stack-more")).toHaveText( "+1", ); const headerStackBox = await page .getByTestId("chat-header-dm-avatar-stack") .boundingBox(); const headerTitleBox = await page.getByTestId("chat-title").boundingBox(); expect(headerStackBox).not.toBeNull(); expect(headerTitleBox).not.toBeNull(); expect((headerStackBox?.x ?? 0) + (headerStackBox?.width ?? 0)).toBeLessThan( headerTitleBox?.x ?? 0, ); await expect(page.getByTestId("message-dm-intro")).toContainText("alice"); await expect(page.getByTestId("message-dm-intro")).toContainText("bob"); await expect(page.getByTestId("message-dm-intro")).toContainText("charlie"); await expect(page.getByTestId("message-dm-intro")).toContainText("+1 more"); await expect(page.getByTestId("message-dm-intro")).not.toContainText( "outsider", ); await expect(page.getByTestId("message-dm-intro-avatar-stack")).toBeVisible(); await expect( page.getByTestId("message-dm-intro-avatar-stack-participant"), ).toHaveCount(3); await expect( page.getByTestId("message-dm-intro-avatar-stack-more"), ).toHaveText("+1"); }); test("create stream with name and description", async ({ page }) => { const channelName = `my-new-stream-${Date.now()}`; await page.goto("/"); await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(channelName); await page .getByTestId("create-channel-description") .fill("A stream for testing channel creation"); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("stream-list")).toContainText(channelName); await expect(page.getByTestId("chat-title")).toHaveText(channelName); }); test("create channel template selector matches the lifecycle controls", async ({ page, }) => { await installMockBridge(page, { channelTemplates: [ { id: "project-kickoff", name: "Project kickoff", description: "Coordinate a new project from planning through launch.", channelType: "stream", visibility: "private", canvasTemplate: "# {channel.name}\n\nKickoff notes", agents: { personas: [ { personaId: "planner", runtime: null, model: null, role: null, backend: null, }, ], teams: [ { teamId: "research-team", runtime: null, model: null, backend: null, }, ], }, isBuiltin: false, createdAt: "2026-07-23T00:00:00Z", updatedAt: "2026-07-23T00:00:00Z", }, ], }); await page.goto("/"); await openCreateChannelDialog(page); const templateControl = page.getByTestId("create-channel-template"); await expect(templateControl).toHaveRole("button"); await expect(templateControl).toHaveText("None"); await templateControl.click(); await expect( page.getByRole("menuitem", { name: "Create new channel template…" }), ).toBeVisible(); await page.getByRole("menuitemradio", { name: "Project kickoff" }).click(); await expect(templateControl).toHaveText("Project kickoff"); await expect(page.getByTestId("create-channel-template-summary")).toHaveText( "Private · Canvas included · 1 agent · 1 team", ); await expect(page.getByTestId("create-channel-description")).toHaveValue( "Coordinate a new project from planning through launch.", ); await expect(page.getByTestId("create-channel-permissions")).toContainText( "Private", ); await page.getByTestId("create-channel-permissions").click(); await page.getByTestId("create-channel-permissions-option-open").click(); await expect(page.getByTestId("create-channel-template-summary")).toHaveText( "Open · Canvas included · 1 agent · 1 team", ); }); test("create channel exposes templates when the library is empty", async ({ page, }) => { await installMockBridge(page, { channelTemplates: [] }); await page.goto("/"); await openCreateChannelDialog(page); const typeContainer = page.getByTestId( "create-channel-channel-type-container", ); const visibilityContainer = page.getByTestId( "create-channel-permissions-container", ); const templateContainer = page.getByTestId( "create-channel-template-container", ); await expect(templateContainer).toContainText("TemplateOptional"); const typeBox = await typeContainer.boundingBox(); const visibilityBox = await visibilityContainer.boundingBox(); const templateBox = await templateContainer.boundingBox(); expect(typeBox).not.toBeNull(); expect(visibilityBox).not.toBeNull(); expect(templateBox).not.toBeNull(); expect(typeBox?.y ?? 0).toBeLessThan(visibilityBox?.y ?? 0); expect(visibilityBox?.y ?? 0).toBeLessThan(templateBox?.y ?? 0); const templateControl = page.getByTestId("create-channel-template"); await expect(templateControl).toHaveText("None"); await templateControl.click(); await page .getByRole("menuitem", { name: "Create new channel template…" }) .click(); await expect( page.getByText("Create template", { exact: true }), ).toBeVisible(); await page.locator("#template-name").fill("Weekly planning"); await page.locator("#template-description").fill("Plan the next week."); await page.getByRole("button", { name: "Create", exact: true }).click(); await expect(templateControl).toHaveText("Weekly planning"); await expect(page.getByTestId("create-channel-description")).toHaveValue( "Plan the next week.", ); }); test("create ephemeral stream shows sidebar and header affordances", async ({ page, }) => { const channelName = `ephemeral-stream-${Date.now()}`; await page.goto("/"); await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(channelName); await page .getByTestId("create-channel-description") .fill("Auto-cleaned test stream"); await page.getByTestId("create-channel-channel-type").click(); await page.getByLabel("Temporary channel").click(); const channelTypeContainer = page.getByTestId( "create-channel-channel-type-container", ); await expect( channelTypeContainer.getByTestId("create-channel-permissions-container"), ).toHaveCount(0); await expect( page.getByTestId("create-channel-permissions-container"), ).toBeVisible(); await expect(page.getByTestId("create-channel-permissions")).toContainText( "Public", ); await page.getByTestId("create-channel-permissions").click(); await page.getByTestId("create-channel-permissions-option-private").click(); await expect(page.getByTestId("create-channel-permissions")).toContainText( "Private", ); await expect( page.getByTestId("create-channel-permissions-option-private"), ).toHaveCount(0); await page.getByTestId("create-channel-permissions").click(); await expect( page.getByTestId("create-channel-permissions-option-open"), ).toBeVisible(); await page.getByTestId("create-channel-permissions-option-open").click(); await expect(page.getByTestId("create-channel-ttl")).toContainText("7 days"); await page.getByTestId("create-channel-ttl").click(); await page.getByTestId("create-channel-ttl-option-1209600").click(); await expect(page.getByTestId("create-channel-ttl")).toContainText("14 days"); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("stream-list")).toContainText(channelName); await expect(page.getByTestId("chat-title")).toContainText(channelName); await expect( page.getByTestId(`channel-ephemeral-${channelName}`), ).toBeVisible(); await expect(page.getByTestId("chat-ephemeral-badge")).toBeVisible(); await expect(page.getByTestId("chat-ephemeral-badge")).toHaveAttribute( "aria-label", /Ephemeral channel\. Cleans up in 14 days\./, ); await page .getByRole("button", { name: "Toggle Sidebar", exact: true }) .click(); await expect( page.getByTestId(`channel-ephemeral-${channelName}`), ).toBeVisible(); }); test("ephemeral countdown refreshes when switching channels after a clock jump", async ({ page, }) => { const firstChannelName = "ephemeral-alpha"; const secondChannelName = "ephemeral-beta"; const initialTime = new Date("2026-04-09T00:00:00.000Z"); const shiftedTime = new Date("2026-04-15T02:00:00.000Z"); await page.clock.setFixedTime(initialTime); await page.goto("/"); for (const channelName of [firstChannelName, secondChannelName]) { await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(channelName); await page .getByTestId("create-channel-description") .fill("Auto-cleaned test stream"); await page.getByTestId("create-channel-channel-type").click(); await page.getByLabel("Temporary channel").click(); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("chat-title")).toContainText(channelName); } await page.clock.setFixedTime(shiftedTime); await expect .poll(() => page.evaluate(() => Date.now())) .toBe(shiftedTime.getTime()); await page.getByTestId(`channel-${firstChannelName}`).click(); await expect(page.getByTestId("chat-title")).toContainText(firstChannelName); await expect(page.getByTestId("chat-ephemeral-badge")).toHaveAttribute( "aria-label", /Ephemeral channel\. Cleans up in 22 hours\./, ); }); test("archived channels stay out of all sidebar sections", async ({ page }) => { const archivedStreamName = `archived-stream-${Date.now()}`; const archivedForumName = `archived-forum-${Date.now()}`; await page.goto("/"); await page.waitForFunction(() => { return Boolean( ( window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown; } ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__, ); }); await page.evaluate( async ({ archivedForumName, archivedStreamName, outsiderPubkey, }: { archivedForumName: string; archivedStreamName: string; outsiderPubkey: string; }) => { const invoke = ( window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: ( command: string, payload?: Record, ) => Promise<{ id: string }>; } ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__; if (!invoke) { throw new Error("Mock bridge is not installed."); } const stream = await invoke("create_channel", { channelType: "stream", name: archivedStreamName, visibility: "open", }); const forum = await invoke("create_channel", { channelType: "forum", name: archivedForumName, visibility: "open", }); const directMessage = await invoke("open_dm", { pubkeys: [outsiderPubkey], }); for (const channel of [stream, forum, directMessage]) { await invoke("archive_channel", { channelId: channel.id }); } }, { archivedForumName, archivedStreamName, outsiderPubkey: TEST_IDENTITIES.outsider.pubkey, }, ); await page.reload(); await expect(page.getByTestId("stream-list")).not.toContainText( archivedStreamName, ); await expect(page.getByTestId("forum-list")).not.toContainText( archivedForumName, ); await expect(page.getByTestId("dm-list")).toContainText("alice-tyler"); await expect(page.getByTestId("dm-list")).not.toContainText("outsider"); }); test("create stream with special characters", async ({ page }) => { const channelName = `dev ops-${Date.now()}`; await page.goto("/"); await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(channelName); await page .getByTestId("create-channel-description") .fill("Stream with spaces and hyphens"); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("stream-list")).toContainText(channelName); await expect(page.getByTestId("chat-title")).toHaveText(channelName); }); test("switch between streams", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await page.getByTestId("channel-engineering").click(); await expect(page.getByTestId("chat-title")).toHaveText("engineering"); }); test("switch between channel types", async ({ page }) => { await page.goto("/"); // Stream await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); // Forum await page.getByTestId("channel-watercooler").click(); await expect(page.getByTestId("chat-title")).toHaveText("watercooler"); // DM await page.getByTestId("channel-alice-tyler").click(); await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); }); test("empty channel shows intro actions", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await expect(page.getByTestId("message-channel-intro")).toBeVisible(); await expect(page.getByTestId("message-channel-intro")).toContainText( "This is the beginning of the regular channel.", ); await expect( page.getByTestId("channel-intro-action-create-channel"), ).toHaveCount(0); await expect( page.getByTestId("channel-intro-action-create-agent"), ).toBeVisible(); await expect( page.getByTestId("channel-intro-action-add-people"), ).toBeVisible(); await expect(page.getByTestId("welcome-composer-guide-banner")).toHaveCount( 0, ); await expectIntroActionCardLayout(page, "channel-intro-action-create-agent"); await expectIntroActionsShareRow(page, [ "channel-intro-action-create-agent", "channel-intro-action-add-people", ]); await page.getByTestId("channel-intro-action-add-people").click(); await expect(page.getByTestId("members-sidebar")).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByTestId("members-sidebar")).not.toBeVisible(); await page.getByTestId("channel-intro-action-create-agent").click(); await expect(page.getByRole("heading", { name: "Add agents" })).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByRole("heading", { name: "Add agents" })).toHaveCount( 0, ); }); test("short channel with messages shows intro actions on open", async ({ page, }) => { const channelName = `short-intro-${Date.now()}`; const message = "Only message in a short channel"; await page.goto("/"); await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(channelName); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); await page.getByTestId("message-input").fill(message); await page.getByTestId("send-message").click(); await expect(page.getByTestId("message-timeline")).toContainText(message); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.getByTestId(`channel-${channelName}`).click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); await expect(page.getByTestId("message-timeline")).toContainText(message); await expect(page.getByTestId("message-channel-intro")).toBeVisible(); await expect(page.getByTestId("message-channel-intro")).toContainText( "This is the beginning of the regular channel.", ); await expect( page.getByTestId("channel-intro-action-create-agent"), ).toBeVisible(); await expect( page.getByTestId("channel-intro-action-add-people"), ).toBeVisible(); }); test("scrollable channel with recent messages hides intro actions until top", async ({ page, }) => { const channelName = `long-intro-${Date.now()}`; const messages = Array.from( { length: 24 }, (_, index) => `Scrollable channel message ${index + 1}`, ); await page.goto("/"); await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(channelName); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); for (const message of messages) { await page.getByTestId("message-input").fill(message); await page.getByTestId("send-message").click(); await expect(page.getByTestId("message-timeline")).toContainText(message); } await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.getByTestId(`channel-${channelName}`).click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); await expect(page.getByTestId("message-channel-intro")).not.toBeInViewport(); await expect( page.getByTestId("channel-intro-action-create-agent"), ).not.toBeInViewport(); await expect( page.getByTestId("channel-intro-action-add-people"), ).not.toBeInViewport(); await expect(page.getByTestId("message-timeline")).toContainText( messages[messages.length - 1], ); await page.getByTestId("message-timeline").evaluate((element) => { element.scrollTop = 0; element.dispatchEvent(new Event("scroll", { bubbles: true })); }); await expect(page.getByTestId("message-channel-intro")).toBeVisible(); await expect( page.getByTestId("channel-intro-action-create-agent"), ).toBeVisible(); await expect( page.getByTestId("channel-intro-action-add-people"), ).toBeVisible(); }); test("channel with messages shows content", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(page.getByTestId("message-channel-intro")).toBeVisible(); await expect( page.getByTestId("channel-intro-action-create-channel"), ).toHaveCount(0); await expect(page.getByTestId("welcome-composer-guide-banner")).toHaveCount( 0, ); await expect(page.getByTestId("message-timeline-day-divider")).toBeVisible(); await expect(page.getByTestId("message-timeline")).toContainText( "Welcome to #general", ); }); test("channel date divider keeps the date sticky while the separator rule scrolls", async ({ page, }) => { await page.goto("/"); await page.getByTestId("channel-engineering").click(); await expect(page.getByTestId("chat-title")).toHaveText("engineering"); await waitForMockLiveSubscription(page, "engineering"); await page.evaluate(() => { const firstDay = 1_700_000_000; for (let day = 0; day < 2; day += 1) { for (let index = 0; index < 14; index += 1) { window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "engineering", content: `date handoff day ${day + 1} row ${index + 1}\nsecond line for scroll height`, createdAt: firstDay + day * 86_400 + index, pubkey: "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f", }); } } }); await expect(page.getByTestId("message-timeline-day-group")).toHaveCount(2); await expect(page.getByTestId("message-timeline-day-divider")).toHaveCount(2); const timeline = page.getByTestId("message-timeline"); await timeline.evaluate((element) => { element.scrollTop = element.scrollHeight * 0.2; element.dispatchEvent(new Event("scroll", { bubbles: true })); }); const [headerBox, stickyPillBox] = await Promise.all([ page.getByTestId("chat-header").boundingBox(), page .getByTestId("message-timeline-sticky-day-divider-content") .locator("p") .first() .boundingBox(), ]); if (!headerBox || !stickyPillBox) { throw new Error("missing channel header or sticky day divider"); } expect( Math.abs(stickyPillBox.y - (headerBox.y + headerBox.height) - 8), ).toBeLessThanOrEqual(1); await expect( page.getByTestId("message-timeline-sticky-day-divider"), ).toHaveCSS("opacity", "1"); await expect( page.getByTestId("message-timeline-day-divider").last().locator("p"), ).toHaveCSS("visibility", "visible"); const metrics = await timeline.evaluate((element) => { const pinnedDivider = element.parentElement?.querySelector( '[data-testid="message-timeline-sticky-day-divider"]', ); const pinnedPill = pinnedDivider?.querySelector( '[data-testid="message-timeline-sticky-day-divider-content"] p', ); if (!pinnedDivider || !pinnedPill) { throw new Error("missing sticky day divider"); } return { dividerPillBackground: getComputedStyle(pinnedPill).backgroundColor, dividerPillShadow: getComputedStyle(pinnedPill).boxShadow, dividerZIndex: getComputedStyle(pinnedDivider).zIndex, }; }); expect(Number.parseInt(metrics.dividerZIndex, 10)).toBeGreaterThan(10); await expect( page.getByTestId("message-timeline-sticky-day-divider"), ).toHaveCSS("overflow", "visible"); const dividerAlignment = await timeline.evaluate((element) => { const group = [ ...element.querySelectorAll( '[data-testid="message-timeline-day-group"]', ), ].find((candidate) => { const pill = candidate.querySelector("p"); return pill && getComputedStyle(pill).visibility === "visible"; }); const pill = group?.querySelector("p"); if (!group || !pill) throw new Error("missing visible day divider"); const rule = getComputedStyle(group, "::before"); const groupBox = group.getBoundingClientRect(); const pillBox = pill.getBoundingClientRect(); return { chipCenter: pillBox.top + pillBox.height / 2, ruleCenter: groupBox.top + Number.parseFloat(rule.top) + Number.parseFloat(rule.height) / 2, }; }); expect( Math.abs(dividerAlignment.chipCenter - dividerAlignment.ruleCenter), ).toBeLessThanOrEqual(0.5); await expect .poll(async () => { const headerZIndex = await page .getByTestId("chat-header") .evaluate( (element) => getComputedStyle(element.parentElement ?? element).zIndex, ); return Number.parseInt(headerZIndex, 10); }) .toBeGreaterThan(Number.parseInt(metrics.dividerZIndex, 10)); await expect .poll(async () => { const sharedBackdropZIndex = await page .getByTestId("channel-shared-header-backdrop") .evaluate((element) => getComputedStyle(element).zIndex); return Number.parseInt(sharedBackdropZIndex, 10); }) .toBeGreaterThan(Number.parseInt(metrics.dividerZIndex, 10)); const composerOverlay = page.getByTestId("channel-composer-overlay"); await expect(composerOverlay).toBeVisible(); await expect(composerOverlay).toHaveCSS("backdrop-filter", "none"); await expect(composerOverlay).toHaveCSS( "background-color", "rgba(0, 0, 0, 0)", ); await expect(composerOverlay.getByTestId("message-composer")).toHaveCSS( "backdrop-filter", "none", ); await expect( composerOverlay.getByTestId("composer-dock-backdrop").locator("div"), ).not.toHaveCSS("backdrop-filter", "none"); const composerRailMask = composerOverlay.getByTestId( "composer-dock-rail-mask", ); await expect(composerRailMask).toHaveCSS("backdrop-filter", "none"); await expect(composerRailMask).not.toHaveCSS( "background-color", "rgba(0, 0, 0, 0)", ); await expect .poll(async () => { const composerOverlayZIndex = await page .getByTestId("channel-composer-overlay") .evaluate((element) => getComputedStyle(element).zIndex); return Number.parseInt(composerOverlayZIndex, 10); }) .toBeGreaterThan(Number.parseInt(metrics.dividerZIndex, 10)); expect(metrics.dividerPillBackground).not.toBe("rgba(0, 0, 0, 0)"); expect(metrics.dividerPillBackground).not.toBe("transparent"); expect(metrics.dividerPillShadow).toBe("none"); }); test("shows and clears activity indicators for active channel agents", async ({ page, }) => { await page.goto("/"); await page.getByTestId("channel-agents").click(); await expect(page.getByTestId("chat-title")).toHaveText("agents"); await waitForMockLiveSubscription(page, "agents", KIND_TYPING_INDICATOR); await page.evaluate((pubkey) => { window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({ channelName: "agents", pubkey, }); }, TEST_IDENTITIES.alice.pubkey); await expect(page.getByTestId("bot-activity-composer-trigger")).toBeVisible(); await expect( page.getByTestId("bot-activity-composer-trigger"), ).not.toContainText("View activity"); await page.getByTestId("bot-activity-composer-trigger").click(); const aliceActivityItem = page.getByTestId( `bot-activity-composer-item-${TEST_IDENTITIES.alice.pubkey}`, ); await expect(aliceActivityItem).toBeVisible(); await expect(aliceActivityItem).toContainText("View activity"); await aliceActivityItem.click({ force: true }); await expect(page.getByTestId("agent-session-thread-panel")).toBeVisible(); await expect(page.getByTestId("agent-session-thread-panel")).toContainText( "alice", ); // Opened from the composer with no prior pane: there is nowhere to go // "back" to, so the header shows only the close affordance. await expect(page.getByTestId("agent-session-back")).toHaveCount(0); await expect(page.getByTestId("auxiliary-panel-close")).toBeVisible(); await expect(page.getByTestId("agent-transcript-now-summary")).toHaveCount(0); await page.getByTestId("agent-session-settings-menu-trigger").click(); await expect(page.getByTestId("agent-session-stop-turn")).toBeVisible(); await expect(page.getByTestId("agent-session-stop-turn")).toBeDisabled(); await page.keyboard.press("Escape"); await expect(page.getByTestId("agent-session-thread-panel")).toContainText( "No ACP activity yet", ); await expect(page.getByTestId("message-typing-indicator")).toHaveCount(0); await page.evaluate((pubkey) => { window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "agents", content: "Done.", pubkey, }); }, TEST_IDENTITIES.alice.pubkey); await expect(page.getByTestId("message-timeline")).toContainText("Done."); await expect(page.getByTestId("bot-activity-composer-trigger")).toHaveCount( 0, ); await page.waitForTimeout(1_200); await page.evaluate((pubkey) => { window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({ channelName: "agents", pubkey, }); }, TEST_IDENTITIES.alice.pubkey); await expect(page.getByTestId("bot-activity-composer-trigger")).toHaveCount( 0, ); }); test("members sidebar exposes view-activity for a viewer-owned relay agent", async ({ page, }) => { // nadia is no longer in the default relay-agent seeds (the no-relay-record // owner path claims that fixture), so seed her relay registry entry here to // exercise the relay-bot classification. await installMockBridge(page, { relayAgents: [ { pubkey: OWNED_RELAY_AGENT_PUBKEY, name: "nadia", agentType: "goose", capabilities: ["search", "summaries"], channelNames: ["agents"], respondTo: "anyone", }, ], }); await page.goto("/"); await openMembersSidebar(page, "agents"); // nadia is a relay-only agent (no local managed record) owned by the mock // viewer. The view-activity affordance must open via the declared-owner // path, not the local-managed path. await expect( page.getByTestId(`sidebar-member-${OWNED_RELAY_AGENT_PUBKEY}`), ).toBeVisible(); await openMemberMenu(page, OWNED_RELAY_AGENT_PUBKEY); await expect( page.getByTestId(`sidebar-view-activity-${OWNED_RELAY_AGENT_PUBKEY}`), ).toBeVisible(); }); test("profile renders live activity for a viewer-owned relay agent", async ({ page, }) => { await page.goto("/"); await openMembersSidebar(page, "agents"); await page .getByTestId(`sidebar-member-open-profile-${OWNED_RELAY_AGENT_PUBKEY}`) .click(); await expect(page.getByTestId("members-sidebar")).not.toBeVisible(); await expect( page.getByTestId(`user-profile-view-activity-${OWNED_RELAY_AGENT_PUBKEY}`), ).toBeVisible(); await page.waitForFunction( () => typeof (window as MockFeedWindow).__BUZZ_E2E_SEED_ACTIVE_TURNS__ === "function", ); await page.evaluate( ({ agentPubkey, channelId }) => { const seedActiveTurns = (window as MockFeedWindow) .__BUZZ_E2E_SEED_ACTIVE_TURNS__; if (!seedActiveTurns) { throw new Error("Mock active-turn helper is not installed."); } seedActiveTurns({ agentPubkey, channelId, turnId: "owned-relay-profile-turn", }); }, { agentPubkey: OWNED_RELAY_AGENT_PUBKEY, channelId: AGENTS_CHANNEL_ID, }, ); const liveActivity = page.getByTestId( `user-profile-live-activity-${OWNED_RELAY_AGENT_PUBKEY}`, ); await expect(liveActivity).toBeVisible(); await expect(liveActivity).toContainText("Latest Activity"); await expect(liveActivity).toContainText("#agents"); await expect( page.getByTestId(`user-profile-activity-dot-${AGENTS_CHANNEL_ID}`), ).toHaveCount(0); await page.evaluate( ({ agentPubkey, channelId, turnId }) => { const seedActiveTurns = (window as MockFeedWindow) .__BUZZ_E2E_SEED_ACTIVE_TURNS__; if (!seedActiveTurns) { throw new Error("Mock active-turn helper is not installed."); } seedActiveTurns({ agentPubkey, channelId, turnId, kind: "turn_completed", }); }, { agentPubkey: OWNED_RELAY_AGENT_PUBKEY, channelId: AGENTS_CHANNEL_ID, turnId: "owned-relay-profile-turn", }, ); await expect(liveActivity).toBeVisible(); await expect(liveActivity).toContainText("Latest Activity"); await expect( page.getByTestId(`user-profile-view-activity-${OWNED_RELAY_AGENT_PUBKEY}`), ).not.toBeVisible(); }); test("profile activity carousel switches channels via progress dots", async ({ page, }) => { await page.goto("/"); await openMembersSidebar(page, "agents"); await page .getByTestId(`sidebar-member-open-profile-${OWNED_RELAY_AGENT_PUBKEY}`) .click(); await expect(page.getByTestId("members-sidebar")).not.toBeVisible(); await expect( page.getByTestId(`user-profile-view-activity-${OWNED_RELAY_AGENT_PUBKEY}`), ).toBeVisible(); await page.waitForFunction( () => typeof (window as MockFeedWindow).__BUZZ_E2E_SEED_ACTIVE_TURNS__ === "function", ); await page.evaluate( ({ agentPubkey, channels }) => { const seedActiveTurns = (window as MockFeedWindow) .__BUZZ_E2E_SEED_ACTIVE_TURNS__; if (!seedActiveTurns) { throw new Error("Mock active-turn helper is not installed."); } for (const [index, channelId] of channels.entries()) { seedActiveTurns({ agentPubkey, channelId, turnId: `owned-relay-profile-turn-${index}`, }); } }, { agentPubkey: OWNED_RELAY_AGENT_PUBKEY, channels: [AGENTS_CHANNEL_ID, GENERAL_CHANNEL_ID], }, ); const liveActivity = page.getByTestId( `user-profile-live-activity-${OWNED_RELAY_AGENT_PUBKEY}`, ); await expect(liveActivity).toBeVisible(); await expect(liveActivity).toContainText("#agents"); await expect( page.getByTestId(`user-profile-activity-dot-${AGENTS_CHANNEL_ID}`), ).toBeVisible(); await expect( page.getByTestId(`user-profile-activity-dot-${GENERAL_CHANNEL_ID}`), ).toBeVisible(); await expect( page.getByTestId(`user-profile-activity-slide-${GENERAL_CHANNEL_ID}`), ).toHaveAttribute("data-mounted", "false"); await page .getByTestId(`user-profile-activity-dot-${GENERAL_CHANNEL_ID}`) .click(); await expect( page.getByTestId("user-profile-activity-channel-label"), ).toContainText("#general"); await expect( page.getByTestId(`user-profile-activity-slide-${GENERAL_CHANNEL_ID}`), ).toHaveAttribute("data-mounted", "true"); await expect( page.getByTestId(`user-profile-activity-slide-${AGENTS_CHANNEL_ID}`), ).toHaveAttribute("data-mounted", "true"); }); test("typing indicator shows avatars and maintains stable name order", async ({ page, }) => { await page.goto("/"); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await waitForMockLiveSubscription(page, "random", KIND_TYPING_INDICATOR); // Alice starts typing first await page.evaluate((pubkey) => { window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({ channelName: "random", pubkey, }); }, TEST_IDENTITIES.alice.pubkey); await expect(page.getByTestId("message-typing-indicator")).toBeVisible(); await expect( page.getByTestId("message-typing-indicator-label"), ).toContainText("alice is typing"); // Verify avatar is rendered for the typing user const avatars = page .getByTestId("message-typing-indicator") .locator("[data-testid='message-typing-avatar']"); await expect(avatars).toHaveCount(1); // Bob starts typing second await page.evaluate((pubkey) => { window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({ channelName: "random", pubkey, }); }, TEST_IDENTITIES.bob.pubkey); await expect( page.getByTestId("message-typing-indicator-label"), ).toContainText("alice and bob are typing"); await expect(avatars).toHaveCount(2); // Alice re-broadcasts — order should stay "alice and bob", not flip await page.evaluate((pubkey) => { window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({ channelName: "random", pubkey, }); }, TEST_IDENTITIES.alice.pubkey); await expect( page.getByTestId("message-typing-indicator-label"), ).toContainText("alice and bob are typing"); // Bob re-broadcasts — order should still stay "alice and bob" await page.evaluate((pubkey) => { window.__BUZZ_E2E_EMIT_MOCK_TYPING__?.({ channelName: "random", pubkey, }); }, TEST_IDENTITIES.bob.pubkey); await expect( page.getByTestId("message-typing-indicator-label"), ).toContainText("alice and bob are typing"); }); test("sidebar shows unread indicator for newly active channels", async ({ page, }) => { await page.goto("/"); await expect(page.getByTestId("channel-unread-random")).toHaveCount(0); await waitForMockLiveSubscription(page, "random"); // The unread tracker ignores the current user's own messages, so emit as // alice — simulating a real "another user posted while I was elsewhere". await page.evaluate( ({ pubkey }) => { window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "random", content: "Unread update for #random", kind: 40002, pubkey, }); }, { pubkey: TEST_IDENTITIES.alice.pubkey }, ); await expect(page.getByTestId("channel-random")).toHaveCSS( "font-weight", "700", ); await expect(page.getByTestId("channel-unread-random")).toHaveCount(0); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await expect(page.getByTestId("message-timeline")).toContainText( "Unread update for #random", ); await expect(page.getByTestId("channel-unread-random")).toHaveCount(0); }); test("sidebar shows unread indicator for new forum posts", async ({ page }) => { await page.goto("/"); await expect(page.getByTestId("channel-unread-watercooler")).toHaveCount(0); await waitForMockLiveSubscription(page, "watercooler"); // Emit as alice — the unread tracker ignores self-authored messages. await page.evaluate( ({ pubkey }) => { window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "watercooler", content: "Unread update for the forum", kind: 45001, pubkey, }); }, { pubkey: TEST_IDENTITIES.alice.pubkey }, ); await expect(page.getByTestId("channel-watercooler")).toHaveCSS( "font-weight", "700", ); await expect(page.getByTestId("channel-unread-watercooler")).toHaveCount(0); await page.getByTestId("channel-watercooler").click(); await expect(page.getByTestId("chat-title")).toHaveText("watercooler"); await expect(page.getByTestId("channel-unread-watercooler")).toHaveCount(0); }); test("sidebar clears unread indicator after opening a DM", async ({ page }) => { await page.goto("/"); await expect(page.getByTestId("channel-unread-alice-tyler")).toHaveCount(0); await waitForMockLiveSubscription(page, "alice-tyler"); await page.evaluate((pubkey) => { window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "alice-tyler", content: "Unread update for the DM", pubkey, }); }, TEST_IDENTITIES.alice.pubkey); await expect(page.getByTestId("channel-unread-alice-tyler")).toBeVisible(); await page.getByTestId("channel-alice-tyler").click(); await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler"); await expect(page.getByTestId("message-dm-intro")).toBeVisible(); await expect(page.getByTestId("message-dm-intro")).toContainText( "This is the beginning of your direct message with", ); await expect(page.getByTestId("message-timeline-day-divider")).toBeVisible(); await expect(page.getByTestId("message-timeline")).toContainText( "Unread update for the DM", ); await expectSameLeftInset(page, "message-dm-intro", "message-row"); await expectIntroSpacedAboveDayDivider(page, "message-dm-intro"); await expect(page.getByTestId("channel-unread-alice-tyler")).toHaveCount(0); }); test("sidebar persists after channel switch", async ({ page }) => { await page.goto("/"); await expect(page.getByTestId("app-sidebar")).toBeVisible(); await page.getByTestId("channel-general").click(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(page.getByTestId("app-sidebar")).toBeVisible(); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await expect(page.getByTestId("app-sidebar")).toBeVisible(); await page.getByTestId("channel-watercooler").click(); await expect(page.getByTestId("chat-title")).toHaveText("watercooler"); await expect(page.getByTestId("app-sidebar")).toBeVisible(); }); test("manage channel updates details", async ({ page }) => { const stamp = Date.now(); const newName = `release-hub-${stamp}`; const newDescription = `Release coordination ${stamp}`; await page.goto("/"); await openChannelManagement(page, "general"); await openChannelEditDialog(page); const editDialog = page.getByRole("dialog", { name: /Edit (?:public|private) channel/, }); await editDialog.getByTestId("channel-management-name").fill(newName); await editDialog .getByTestId("channel-management-description") .fill(newDescription); await expect(editDialog.getByTestId("channel-management-topic")).toHaveCount( 0, ); await expect( editDialog.getByTestId("channel-management-purpose"), ).toHaveCount(0); await editDialog.getByTestId("channel-management-save-changes").click(); await expect(editDialog).toHaveCount(0); await expect(page.getByTestId("chat-title")).toHaveText(newName); await expect(page.getByTestId("stream-list")).toContainText(newName); await expect(page.getByTestId("channel-management-name-row")).toContainText( newName, ); await expect( page.getByTestId("channel-management-description"), ).toContainText(newDescription); await closeChannelManagement(page); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await page.getByTestId("stream-list").getByText(newName).click(); await expect(page.getByTestId("chat-title")).toHaveText(newName); await page.getByTestId("channel-management-trigger").click(); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); await openChannelEditDialog(page); const reopenedEditDialog = page.getByRole("dialog", { name: /Edit (?:public|private) channel/, }); await expect( reopenedEditDialog.getByTestId("channel-management-name"), ).toHaveValue(newName); await expect( reopenedEditDialog.getByTestId("channel-management-description"), ).toHaveValue(newDescription); await expect( reopenedEditDialog.getByTestId("channel-management-topic"), ).toHaveCount(0); await expect( reopenedEditDialog.getByTestId("channel-management-purpose"), ).toHaveCount(0); }); test("manage channel updates visibility and ephemeral lifecycle independently", async ({ page, }) => { await page.goto("/"); await openChannelManagement(page, "general"); await openChannelEditDialog(page); let saveChangesButton = page.getByTestId("channel-management-save-changes"); await expect(saveChangesButton).toBeDisabled(); await page.getByTestId("channel-management-permissions").click(); await page .getByTestId("channel-management-permissions-option-private") .click(); await expect( page.getByRole("dialog", { name: "Edit private channel" }), ).toBeVisible(); await expect( page.getByTestId("channel-management-permissions"), ).toHaveAccessibleName("Visibility: Private"); await page.getByTestId("channel-management-channel-type").click(); await page.getByLabel("Temporary channel").click(); await expect(page.getByTestId("channel-management-ttl")).toBeVisible(); await expect(saveChangesButton).toBeEnabled(); const commandCountBeforeEnable = (await readCommandPayloadLog(page)).length; await saveChangesButton.click(); await expect .poll(async () => (await readCommandPayloadLog(page)).slice(commandCountBeforeEnable), ) .toContainEqual( expect.objectContaining({ command: "update_channel", payload: expect.objectContaining({ input: expect.objectContaining({ ttlSeconds: 604800 }), }), }), ); await expect( page.getByRole("dialog", { name: /Edit (?:public|private) channel/, }), ).toHaveCount(0); const channelAfterEnable = await invokeMockCommand<{ ttl_seconds: number | null; visibility: string; }>(page, "get_channel_details", { channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", }); expect(channelAfterEnable).toMatchObject({ ttl_seconds: 604800, visibility: "private", }); await closeChannelManagement(page); await openChannelManagement(page, "general"); await openChannelEditDialog(page); saveChangesButton = page.getByTestId("channel-management-save-changes"); await expect( page.getByTestId("channel-management-permissions"), ).toHaveAccessibleName("Visibility: Private"); await expect( page.getByTestId("channel-management-channel-type"), ).toContainText("Temporary"); await expect(page.getByTestId("channel-management-ttl")).toContainText( "7 days", ); await page.getByTestId("channel-management-permissions").click(); await page.getByTestId("channel-management-permissions-option-open").click(); await expect( page.getByRole("dialog", { name: "Edit public channel" }), ).toBeVisible(); await expect( page.getByTestId("channel-management-permissions"), ).toHaveAccessibleName("Visibility: Public"); await page.getByTestId("channel-management-channel-type").click(); await page.getByLabel("Ongoing channel").click(); await expect(saveChangesButton).toBeEnabled(); const commandCountBeforeDisable = (await readCommandPayloadLog(page)).length; await saveChangesButton.click(); await expect .poll(async () => (await readCommandPayloadLog(page)).slice(commandCountBeforeDisable), ) .toContainEqual( expect.objectContaining({ command: "update_channel", payload: expect.objectContaining({ input: expect.objectContaining({ ttlSeconds: null }), }), }), ); await expect( page.getByRole("dialog", { name: /Edit (?:public|private) channel/, }), ).toHaveCount(0); const channelAfterDisable = await invokeMockCommand<{ ttl_seconds: number | null; visibility: string; }>(page, "get_channel_details", { channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", }); expect(channelAfterDisable).toMatchObject({ ttl_seconds: null, visibility: "open", }); await closeChannelManagement(page); await openChannelManagement(page, "general"); await openChannelEditDialog(page); await expect( page.getByTestId("channel-management-permissions"), ).toHaveAccessibleName("Visibility: Public"); await expect( page.getByTestId("channel-management-channel-type"), ).toContainText("Ongoing"); await expect(page.getByTestId("channel-management-ttl")).toHaveCount(0); }); test("manage channel keeps canvas near the top of the sheet", async ({ page, }) => { await page.goto("/"); await openChannelManagement(page, "general"); const sheet = page.getByTestId("channel-management-sheet"); const sheetBox = await sheet.boundingBox(); const timelineBox = await page.getByTestId("message-timeline").boundingBox(); // Canvas ingress should appear before the channel metadata rows in the DOM. const canvasBox = await sheet .getByTestId("channel-canvas-ingress") .boundingBox(); const nameBox = await sheet .getByTestId("channel-management-name-row") .boundingBox(); expect(sheetBox).not.toBeNull(); expect(timelineBox).not.toBeNull(); if (!sheetBox || !timelineBox) { throw new Error("Expected channel management panel and timeline boxes."); } expect(timelineBox.x + timelineBox.width).toBeLessThanOrEqual(sheetBox.x + 1); await page.mouse.click(timelineBox.x + 24, timelineBox.y + 180); await expect(sheet).toBeVisible(); await page.keyboard.press("Escape"); await expect(sheet).toBeVisible(); await page.setViewportSize({ height: 720, width: 820 }); await expect(page.getByTestId("message-timeline")).toHaveCount(0); await expect(page.getByTestId("channel-drop-zone")).toHaveCount(0); await expect(sheet).toBeVisible(); const narrowSheetBox = await sheet.boundingBox(); if (!narrowSheetBox) { throw new Error("Expected narrow channel management panel box."); } expect(narrowSheetBox.width).toBeGreaterThan(500); expect(canvasBox).not.toBeNull(); expect(nameBox).not.toBeNull(); expect(canvasBox?.y).toBeLessThan(nameBox?.y); }); async function seedHomeInboxMention( page: import("@playwright/test").Page, itemId: string, tags?: string[][], ) { await page.goto("/"); await expect(page.getByTestId("home-inbox-list")).toBeVisible(); await page.waitForFunction( () => typeof (window as MockFeedWindow).__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function", ); await page.evaluate( ({ channelId, createdAt, currentPubkey, itemId: id, senderPubkey, tags: seededTags, }) => { const pushFeedItem = (window as MockFeedWindow) .__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__; if (!pushFeedItem) { throw new Error("Mock feed injection helper is not installed."); } pushFeedItem({ id, kind: 9, pubkey: senderPubkey, content: "Please review the home panel routing.", created_at: createdAt, channel_id: channelId, channel_name: "general", tags: seededTags ?? [ ["e", channelId], ["p", currentPubkey], ], category: "mention", }); }, { channelId: GENERAL_CHANNEL_ID, createdAt: Math.floor(Date.now() / 1000), currentPubkey: TEST_IDENTITIES.tyler.pubkey, itemId, senderPubkey: TEST_IDENTITIES.alice.pubkey, tags, }, ); await page.getByTestId(`home-inbox-item-${itemId}`).click(); } test("Inbox All excludes generic channel traffic", async ({ page }) => { await page.goto("/"); await page.waitForFunction(() => { const win = window as MockFeedWindow; return typeof win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function"; }); await page.evaluate( ({ channelId, currentPubkey, senderPubkey }) => { const pushFeedItem = (window as MockFeedWindow) .__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__; if (!pushFeedItem) throw new Error("Mock feed helper is not installed."); const now = Math.floor(Date.now() / 1000); pushFeedItem({ category: "activity", channel_id: channelId, channel_name: "general", channel_type: "stream", content: "Ordinary channel traffic", created_at: now, id: "inbox-generic-channel-message", kind: 9, pubkey: senderPubkey, tags: [["h", channelId]], }); pushFeedItem({ category: "mention", channel_id: channelId, channel_name: "general", channel_type: "stream", content: "A message that needs my attention", created_at: now + 1, id: "inbox-personal-mention", kind: 9, pubkey: senderPubkey, tags: [ ["h", channelId], ["p", currentPubkey], ], }); }, { channelId: GENERAL_CHANNEL_ID, currentPubkey: TEST_IDENTITIES.tyler.pubkey, senderPubkey: TEST_IDENTITIES.alice.pubkey, }, ); await expect( page.getByTestId("home-inbox-item-inbox-personal-mention"), ).toBeVisible(); await expect( page.getByTestId("home-inbox-item-inbox-generic-channel-message"), ).toHaveCount(0); }); test("Inbox All never lists drafts and unread-only hides reminders", async ({ page, }) => { const draftKey = `channel:${GENERAL_CHANNEL_ID}`; await page.addInitScript( ({ draftStoreKey, draftStorageKey }) => { const timestamp = new Date().toISOString(); window.localStorage.setItem( draftStoreKey, JSON.stringify({ [draftStorageKey]: { channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", content: "Finish the mixed Inbox test", createdAt: timestamp, pendingImeta: [], selectionEnd: 30, selectionStart: 30, spoileredAttachmentUrls: [], status: "active", updatedAt: timestamp, }, }), ); }, { draftStorageKey: draftKey, draftStoreKey: `buzz-drafts.v2:ws://localhost:3000:${MOCK_IDENTITY_PUBKEY}`, }, ); await page.goto("/"); await page.waitForFunction(() => { const win = window as MockFeedWindow; return typeof win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function"; }); const reminderId = "inbox-unread-only-reminder"; const messageId = "inbox-unread-only-message"; await page.evaluate( async ({ channelId, currentPubkey, messageId, reminderId, senderPubkey, }) => { const now = Math.floor(Date.now() / 1_000); window.__BUZZ_E2E_SEED_MOCK_REMINDERS__?.([ { id: reminderId, pubkey: currentPubkey, created_at: now - 120, kind: 30300, tags: [ ["d", reminderId], ["not_before", String(now - 60)], ], content: JSON.stringify({ target: { eventId: "mock-general-alice", channelId, preview: "Due reminder in mixed Inbox", authorPubkey: senderPubkey, }, status: "pending", }), sig: "mocksig".repeat(20).slice(0, 128), }, ]); await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({ queryKey: ["reminders"], }); const pushFeedItem = (window as MockFeedWindow) .__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__; if (!pushFeedItem) throw new Error("Mock feed helper is not installed."); pushFeedItem({ category: "mention", channel_id: channelId, channel_name: "general", channel_type: "stream", content: "Unread message in mixed Inbox", created_at: now, id: messageId, kind: 9, pubkey: senderPubkey, tags: [ ["h", channelId], ["p", currentPubkey], ], }); }, { channelId: GENERAL_CHANNEL_ID, currentPubkey: MOCK_IDENTITY_PUBKEY, messageId, reminderId, senderPubkey: TEST_IDENTITIES.alice.pubkey, }, ); const messageRow = page.getByTestId(`home-inbox-item-${messageId}`); const reminderRow = page.getByTestId(`home-all-reminders-${reminderId}`); const draftRow = page.getByTestId(`home-all-drafts-${draftKey}`); await expect(messageRow).toBeVisible(); await expect(reminderRow).toBeVisible(); // Drafts belong to the dedicated Drafts filter — never the mixed All view. await expect(draftRow).toHaveCount(0); await page.getByTestId("inbox-options-trigger").click(); await page.getByRole("switch", { name: "Show unread only" }).click(); await expect(messageRow).toBeVisible(); await expect(reminderRow).toHaveCount(0); await expect(draftRow).toHaveCount(0); // The draft is still reachable under the Drafts filter. await page.keyboard.press("Escape"); await page.getByTestId("inbox-filter-trigger").click(); await page.getByRole("menuitemradio", { name: "Drafts" }).click(); await expect(page.getByTestId("home-inbox-drafts")).toBeVisible(); }); test("Inbox merges a due reminder into its represented conversation", async ({ page, }) => { const messageId = "inbox-reminder-merge-message"; const reminderId = "inbox-reminder-merge"; await seedHomeInboxMention(page, messageId); await page.evaluate( async ({ authorPubkey, channelId, messageId: targetEventId, pubkey, reminderId: id, }) => { const now = Math.floor(Date.now() / 1_000); window.__BUZZ_E2E_SEED_MOCK_REMINDERS__?.([ { id, pubkey, created_at: now - 600, kind: 30300, tags: [ ["d", id], ["not_before", String(now - 60)], ], content: JSON.stringify({ target: { eventId: targetEventId, channelId, preview: "Please review the home panel routing.", authorPubkey, }, status: "pending", }), sig: "mocksig".repeat(20).slice(0, 128), }, ]); await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({ queryKey: ["reminders"], }); }, { authorPubkey: TEST_IDENTITIES.alice.pubkey, channelId: GENERAL_CHANNEL_ID, messageId, pubkey: MOCK_IDENTITY_PUBKEY, reminderId, }, ); const conversationRow = page.getByTestId(`home-inbox-item-${messageId}`); await expect(conversationRow.getByText("Reminder due")).toBeVisible(); await expect( page.getByTestId(`home-all-reminders-${reminderId}`), ).toHaveCount(0); await page.getByTestId("inbox-filter-trigger").click(); await page.getByRole("menuitemradio", { name: "Mentions" }).click(); await expect(conversationRow.getByText("Reminder due")).toBeVisible(); }); test("Inbox All keeps its filter when opening a due reminder", async ({ page, }) => { await page.goto("/"); await expect(page.getByTestId("home-inbox")).toBeVisible(); const reminderId = "inbox-stable-reminder"; await page.evaluate( async ({ channelId, id, pubkey }) => { const now = Math.floor(Date.now() / 1000); window.__BUZZ_E2E_SEED_MOCK_REMINDERS__?.([ { id, pubkey, created_at: now - 300, kind: 30300, tags: [ ["d", id], ["not_before", String(now - 60)], ], content: JSON.stringify({ target: { eventId: "mock-general-alice", channelId, preview: "Review the Inbox behavior", authorPubkey: pubkey, }, status: "pending", }), sig: "mocksig".repeat(20).slice(0, 128), }, ]); await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({ queryKey: ["reminders"], }); }, { channelId: GENERAL_CHANNEL_ID, id: reminderId, pubkey: MOCK_IDENTITY_PUBKEY, }, ); const reminderRow = page.getByTestId(`home-all-reminders-${reminderId}`); await expect(reminderRow).toBeVisible(); await reminderRow.click(); await expect(page.getByTestId("inbox-filter-trigger")).toContainText("All"); await expect(page.getByTestId("home-reminder-detail")).toBeVisible(); await expect(page.getByTestId("home-inbox-list")).toBeVisible(); }); test("Inbox reminder rows and detail identify DM context", async ({ page }) => { await page.goto("/"); await expect(page.getByTestId("home-inbox")).toBeVisible(); const reminderId = "inbox-dm-reminder"; const dmChannelId = "f48efb06-0c93-5025-aac9-2e646bb6bfa8"; await page.evaluate( async ({ authorPubkey, channelId, currentPubkey, reminderId }) => { const now = Math.floor(Date.now() / 1_000); window.__BUZZ_E2E_SEED_MOCK_REMINDERS__?.([ { id: reminderId, pubkey: currentPubkey, created_at: now - 300, kind: 30300, tags: [ ["d", reminderId], ["not_before", String(now - 60)], ], content: JSON.stringify({ target: { eventId: "mock-dm-alice", channelId, preview: "Follow up with Alice", authorPubkey, }, status: "pending", }), sig: "mocksig".repeat(20).slice(0, 128), }, ]); await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({ queryKey: ["reminders"], }); }, { authorPubkey: TEST_IDENTITIES.alice.pubkey, channelId: dmChannelId, currentPubkey: MOCK_IDENTITY_PUBKEY, reminderId, }, ); await page.getByTestId("inbox-filter-trigger").click(); await page.getByRole("menuitemradio", { name: "Reminders" }).click(); const reminderRow = page.getByTestId(`home-reminder-item-${reminderId}`); await expect( reminderRow.getByText("DM with alice-tyler", { exact: true }), ).toBeVisible(); await reminderRow.getByRole("button").click(); const detail = page.getByTestId("home-reminder-detail"); await expect( detail.getByText("DM with alice-tyler", { exact: true }), ).toBeVisible(); await expect(detail.getByText("#alice-tyler", { exact: true })).toHaveCount( 0, ); }); test("Inbox detail title and source action navigate to the conversation", async ({ page, }) => { await seedHomeInboxMention(page, "mock-feed-home-channel-navigate"); const detail = page.getByTestId("home-inbox-detail"); await expect(detail.getByRole("heading")).toHaveText("Message in #general"); await expect( detail.getByRole("button", { name: "Open in channel" }), ).toBeVisible(); await detail.getByTestId("home-inbox-context-title").click(); await expect(page).toHaveURL( new RegExp(`#/channels/${GENERAL_CHANNEL_ID}\\?`), ); await expect(page).toHaveURL(/messageId=mock-feed-home-channel-navigate/); await expect(page).not.toHaveURL(/threadRootId=/); await expect(page.getByTestId("message-timeline")).toBeVisible(); await expect(page.getByTestId("home-inbox-list")).toHaveCount(0); }); test("home inbox thread reply mention carries threadRootId to the channel", async ({ page, }) => { const rootEventId = "mock-feed-home-thread-root"; await seedHomeInboxMention(page, "mock-feed-home-thread-navigate", [ ["e", rootEventId, "", "root"], ["e", "mock-feed-home-thread-parent", "", "reply"], ["p", TEST_IDENTITIES.tyler.pubkey], ]); const detail = page.getByTestId("home-inbox-detail"); await expect(detail.getByRole("heading")).toHaveText("Thread in #general"); await expect(detail.getByTestId("message-unread-divider")).toBeVisible(); await detail.getByRole("button", { name: "Open full thread" }).click(); await expect(page).toHaveURL( new RegExp(`#/channels/${GENERAL_CHANNEL_ID}\\?`), ); await expect(page).toHaveURL(/messageId=mock-feed-home-thread-navigate/); await expect(page).toHaveURL(new RegExp(`threadRootId=${rootEventId}`)); await expect(page.getByTestId("message-timeline")).toBeVisible(); await expect(page.getByTestId("home-inbox-list")).toHaveCount(0); }); test("Inbox filter changes preserve valid detail and directly select a replacement", async ({ page, }) => { const threadItemId = "inbox-filter-thread"; const actionItemId = "inbox-filter-action"; await seedHomeInboxMention(page, threadItemId, [ ["e", "inbox-filter-root", "", "root"], ["e", "inbox-filter-parent", "", "reply"], ["p", TEST_IDENTITIES.tyler.pubkey], ]); await page.evaluate( ({ actionId, channelId, senderPubkey }) => { const pushFeedItem = (window as MockFeedWindow) .__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__; if (!pushFeedItem) throw new Error("Mock feed helper is not installed."); pushFeedItem({ category: "needs_action", channel_id: channelId, channel_name: "general", channel_type: "stream", content: "Approve the replacement selection", created_at: Math.floor(Date.now() / 1_000) + 120, id: actionId, kind: 46010, pubkey: senderPubkey, tags: [["h", channelId]], }); }, { actionId: actionItemId, channelId: GENERAL_CHANNEL_ID, senderPubkey: TEST_IDENTITIES.alice.pubkey, }, ); await page.getByTestId("inbox-filter-trigger").click(); await page.getByRole("menuitemradio", { name: "Threads" }).click(); await expect( page.getByTestId(`home-inbox-item-${threadItemId}`), ).toHaveAttribute("aria-current", "true"); await expect(page.getByTestId("home-inbox-detail")).toContainText( "Please review the home panel routing.", ); await expect( page.getByRole("menuitemradio", { name: "Threads" }), ).toHaveCount(0); await page.getByTestId("inbox-filter-trigger").click(); await page.getByRole("menuitemradio", { name: "Needs action" }).click(); await expect( page.getByTestId(`home-inbox-item-${actionItemId}`), ).toHaveAttribute("aria-current", "true"); await expect(page.getByTestId("home-inbox-detail")).toContainText( "Approve the replacement selection", ); }); test("Inbox keeps the unread boundary for replies from multiple agents", async ({ page, }) => { await page.goto("/"); await expect(page.getByTestId("home-inbox-list")).toBeVisible(); await page.waitForFunction(() => { const win = window as MockFeedWindow; return ( typeof win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" && typeof win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function" ); }); const replyIds = [ "inbox-agent-reply-first", "inbox-agent-reply-second", "inbox-agent-reply-third", ]; await page.evaluate( ({ agentPubkeys, channelId, currentPubkey, ids }) => { const win = window as MockFeedWindow; const emitMessage = win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__; const pushFeedItem = win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__; if (!emitMessage || !pushFeedItem) { throw new Error("Mock bridge helpers are not installed."); } const createdAt = Math.floor(Date.now() / 1000) + 60; const root = emitMessage({ channelName: "general", content: "Agent collaboration thread", createdAt: createdAt - 10, id: "inbox-agent-thread-root", pubkey: currentPubkey, }); const contents = [ "First unread agent reply", "Second unread agent reply", "Third unread agent reply", ]; contents.forEach((content, index) => { const event = emitMessage({ channelName: "general", content, createdAt: createdAt + index, id: ids[index], parentEventId: root.id, pubkey: agentPubkeys[index % agentPubkeys.length], }); pushFeedItem({ category: "activity", channel_id: channelId, channel_name: "general", channel_type: "stream", content: event.content, created_at: event.created_at, id: event.id, kind: event.kind, pubkey: event.pubkey, tags: event.tags, }); }); }, { agentPubkeys: [ TEST_IDENTITIES.alice.pubkey, TEST_IDENTITIES.charlie.pubkey, ], channelId: GENERAL_CHANNEL_ID, currentPubkey: MOCK_IDENTITY_PUBKEY, ids: replyIds, }, ); const firstUnreadRow = page.getByTestId(`home-inbox-item-${replyIds[0]}`); await expect(firstUnreadRow).toBeVisible(); await firstUnreadRow.click(); const detail = page.getByTestId("home-inbox-detail"); await expect(detail).toContainText("Agent collaboration thread"); await expect(detail).toContainText("First unread agent reply"); await expect(detail).toContainText("Second unread agent reply"); await expect(detail).toContainText("Third unread agent reply"); await expect(detail.getByTestId("message-unread-divider")).toBeVisible(); await expect(page.getByTestId("home-inbox-selected-message")).toContainText( "First unread agent reply", ); }); test("home inbox groups consecutive DMs and opens the full conversation", async ({ page, }) => { const dmChannelId = "f48efb06-0c93-5025-aac9-2e646bb6bfa8"; const dmIds = ["inbox-dm-first", "inbox-dm-second", "inbox-dm-third"]; await page.goto("/"); await expect(page.getByTestId("home-inbox-list")).toBeVisible(); await page.waitForFunction(() => { const win = window as MockFeedWindow; return ( typeof win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function" && typeof win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function" ); }); await page.evaluate( ({ channelId, createdAt, ids, senderPubkey }) => { const win = window as MockFeedWindow; const emitMessage = win.__BUZZ_E2E_EMIT_MOCK_MESSAGE__; const pushFeedItem = win.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__; if (!emitMessage || !pushFeedItem) { throw new Error("Mock bridge helpers are not installed."); } ["First unread DM", "Second unread DM", "Third unread DM"].forEach( (content, index) => { const event = emitMessage({ channelName: "alice-tyler", content, createdAt: createdAt + index, id: ids[index], pubkey: senderPubkey, }); pushFeedItem({ category: "activity", channel_id: channelId, channel_name: "alice-tyler", channel_type: null, content: event.content, created_at: event.created_at, id: event.id, kind: event.kind, pubkey: event.pubkey, tags: event.tags, }); }, ); }, { channelId: dmChannelId, createdAt: Math.floor(Date.now() / 1000), ids: dmIds, senderPubkey: TEST_IDENTITIES.alice.pubkey, }, ); const firstDmRow = page.getByTestId(`home-inbox-item-${dmIds[0]}`); await expect(firstDmRow).toBeVisible(); await expect(page.getByTestId(`home-inbox-item-${dmIds[1]}`)).toHaveCount(0); await expect(page.getByTestId(`home-inbox-item-${dmIds[2]}`)).toHaveCount(0); await expect(firstDmRow.getByTestId("home-inbox-unread-count")).toHaveText( "3 unread", ); await firstDmRow.click(); const detail = page.getByTestId("home-inbox-detail"); await expect(detail.getByRole("heading")).toHaveText("DM with alice"); await expect(detail).toContainText("First unread DM"); await expect(detail).toContainText("Second unread DM"); await expect(detail).toContainText("Third unread DM"); await expect(page.getByTestId("home-inbox-selected-message")).toContainText( "First unread DM", ); const unreadBoundary = page.getByTestId("message-unread-divider"); await expect(unreadBoundary).toBeVisible(); await expect(unreadBoundary).toContainText("New"); await expect( detail.getByRole("button", { name: "Open conversation" }), ).toBeVisible(); }); test("home inbox manage affordance opens management without leaving home", async ({ page, }) => { await seedHomeInboxMention(page, "mock-feed-home-channel-panel"); await page .getByTestId("home-inbox-detail") .getByTestId("channel-management-trigger") .click(); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); await expect(page.getByTestId("channel-management-name-row")).toContainText( "general", ); await expect(page.getByTestId("home-inbox-list")).toBeVisible(); const detailBox = await page.getByTestId("home-inbox-detail").boundingBox(); const sheetBox = await page .getByTestId("channel-management-sheet") .boundingBox(); if (!detailBox || !sheetBox) { throw new Error("Expected home detail pane and channel management boxes."); } expect(detailBox.x + detailBox.width).toBeLessThanOrEqual(sheetBox.x + 1); expect(sheetBox.width).toBeGreaterThanOrEqual(300); const inboxList = page.getByTestId("home-inbox-list"); const inboxListBox = await inboxList.boundingBox(); if (!inboxListBox) { throw new Error("Expected the home inbox list box."); } await inboxList.click({ position: { x: 24, y: inboxListBox.height - 24 }, }); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); await page.keyboard.press("Escape"); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); await page.setViewportSize({ height: 720, width: 820 }); await expect(page.getByTestId("home-inbox-list")).toHaveCount(0); const narrowHomeBox = await page.getByTestId("home-inbox").boundingBox(); const narrowSheetBox = await page .getByTestId("channel-management-sheet") .boundingBox(); if (!narrowHomeBox || !narrowSheetBox) { throw new Error("Expected narrow home and channel management boxes."); } expect(narrowSheetBox.width).toBeGreaterThanOrEqual(narrowHomeBox.width - 1); await expect(page).not.toHaveURL(/#\/channels\//); }); test("members sidebar virtualizes large channel rosters", async ({ page }) => { await page.goto("/"); const channelId = await page .getByTestId("channel-random") .getAttribute("data-channel-id"); if (!channelId) { throw new Error("Random channel id missing."); } const pubkeys = Array.from({ length: 500 }, (_, index) => (index + 1).toString(16).padStart(64, "0"), ); await invokeMockCommand(page, "add_channel_members", { channelId, pubkeys, role: "member", }); await openMembersSidebar(page, "random"); const memberList = page.getByTestId("members-sidebar-people"); const memberRows = memberList.locator('[data-testid^="sidebar-member-"]'); await expect(memberRows.first()).toBeVisible(); expect(await memberRows.count()).toBeLessThan(50); const virtualizedList = memberList.locator(".overflow-y-auto"); await virtualizedList.evaluate((element) => { element.scrollTop = element.scrollHeight; element.dispatchEvent(new Event("scroll")); }); await expect( memberList.getByTestId(`sidebar-member-${pubkeys.at(-1)}`), ).toBeVisible(); }); test("members sidebar can invite relay-authorized agents", async ({ page }) => { await installMockBridge(page, { relayAgents: [ { pubkey: DM_RELAY_AGENT_PUBKEY, name: "quinn", respondTo: "allowlist", respondToAllowlist: [MOCK_IDENTITY_PUBKEY], }, ], }); await page.goto("/"); await openMembersSidebar(page, "general"); await page.getByTestId("channel-management-search-users").fill("quinn"); await expect( page.getByTestId(`channel-user-search-result-${DM_RELAY_AGENT_PUBKEY}`), ).toBeVisible(); }); test("members sidebar hides relay agents that are not authorized", async ({ page, }) => { await installMockBridge(page, { relayAgents: [ { pubkey: DM_RELAY_AGENT_PUBKEY, name: "quinn", respondTo: "allowlist", respondToAllowlist: [TEST_IDENTITIES.outsider.pubkey], }, ], }); await page.goto("/"); await openMembersSidebar(page, "general"); await page.getByTestId("channel-management-search-users").fill("quinn"); await expect( page.getByTestId(`channel-user-search-result-${DM_RELAY_AGENT_PUBKEY}`), ).toHaveCount(0); }); test("members sidebar can invite and remove managed agents", async ({ page, }) => { await installMockBridge(page, { managedAgents: [ { pubkey: TEST_IDENTITIES.charlie.pubkey, name: "charlie", status: "stopped", }, ], }); await page.goto("/"); await openMembersSidebar(page, "general"); const initialMemberCount = await readMembersTriggerCount(page); await expect(page.getByTestId("channel-management-add-pubkeys")).toHaveCount( 0, ); await expect(page.getByText(/Members · \d+/)).toBeVisible(); await page.getByTestId("channel-management-search-users").fill("a"); await expect(page.getByText("Members", { exact: true })).toBeVisible(); await expect(page.getByText(/Members · \d+/)).toHaveCount(0); await expect( page.getByText("Not in this channel", { exact: true }), ).toBeVisible(); await page.getByTestId("channel-management-search-users").fill("char"); await expect( page.getByTestId( `channel-user-search-result-${TEST_IDENTITIES.charlie.pubkey}`, ), ).toBeVisible(); await page .getByTestId(`channel-user-search-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); await expect( page.getByTestId(`sidebar-member-${TEST_IDENTITIES.charlie.pubkey}`), ).toContainText("charlie"); await expect( page.getByTestId(`selected-invitee-${TEST_IDENTITIES.charlie.pubkey}`), ).toHaveCount(0); await expect(page.getByTestId("channel-management-add-members")).toHaveCount( 0, ); await expect(page.getByTestId("channel-management-search-users")).toHaveValue( "char", ); await expectMembersTriggerCount(page, initialMemberCount + 1); await openMemberMenu(page, TEST_IDENTITIES.charlie.pubkey); await page .getByTestId(`sidebar-remove-member-${TEST_IDENTITIES.charlie.pubkey}`) .click(); await expect( page.getByTestId(`sidebar-member-${TEST_IDENTITIES.charlie.pubkey}`), ).toHaveCount(0); await expectMembersTriggerCount(page, initialMemberCount); }); test("members sidebar pages add-member search beyond the first 50 people", async ({ page, }) => { const searchProfiles = Array.from({ length: 55 }, (_, index) => ({ pubkey: `${(index + 1).toString(16).padStart(64, "0")}`, displayName: `Alex ${String(index + 1).padStart(2, "0")}`, })); await installMockBridge(page, { searchProfiles }); await page.goto("/"); await openMembersSidebar(page, "general"); await page.getByTestId("channel-management-search-users").fill("Alex"); const results = page.locator("[data-testid^='channel-user-search-result-']"); await expect(results).toHaveCount(50); await page .getByTestId("members-sidebar-people") .evaluate((node) => node.scrollTo(0, node.scrollHeight)); await expect(results).toHaveCount(55); await expect(page.getByText("Alex 55")).toBeVisible(); const searchCalls = (await readCommandPayloadLog(page)).filter( (entry) => entry.command === "search_users", ); expect(searchCalls).toEqual( expect.arrayContaining([ expect.objectContaining({ payload: expect.objectContaining({ cursor: null, limit: 50 }), }), expect.objectContaining({ payload: expect.objectContaining({ cursor: "2", limit: 50 }), }), ]), ); }); test("shows loading skeletons without stale recipient results", async ({ page, }) => { await installMockBridge(page, { managedAgents: [], relayAgents: [], userSearchDelayMs: 1_000, }); await page.goto("/"); await openNewMessagePage(page); const search = page.getByTestId("new-dm-search"); await search.fill("Alex"); await expect(page.getByTestId("new-dm-loading")).toBeVisible(); await expect( page.locator("[data-testid^='new-dm-result-']:visible"), ).toHaveCount(0); await expect(search).not.toHaveAttribute("aria-activedescendant"); await search.press("Enter"); await expect( page.locator("button[data-testid^='new-dm-selected-']"), ).toHaveCount(0); await expect(page.getByTestId("new-dm-loading")).toBeHidden(); await expect(page.locator("[data-testid^='new-dm-result-']")).toHaveCount(0); await expect(page.getByTestId("new-dm-empty")).toContainText( "No matching users.", ); }); test("new DM picker pages people search beyond the first 50 results", async ({ page, }) => { const searchProfiles = Array.from({ length: 55 }, (_, index) => ({ pubkey: `${(index + 1).toString(16).padStart(64, "0")}`, displayName: `Alex ${String(index + 1).padStart(2, "0")}`, })); await installMockBridge(page, { searchProfiles }); await page.goto("/"); await openNewMessagePage(page); await expect(page.getByTestId("new-message-page")).toBeVisible(); await page.getByTestId("new-dm-search").fill("Alex"); const results = page.locator("[data-testid^='new-dm-result-']"); await expect(results).toHaveCount(50); await page .getByTestId("new-dm-results") .evaluate((node) => node.scrollTo(0, node.scrollHeight)); await expect(results).toHaveCount(55); await expect(page.getByText("Alex 55")).toBeVisible(); const searchCalls = (await readCommandPayloadLog(page)).filter( (entry) => entry.command === "search_users", ); expect(searchCalls).toEqual( expect.arrayContaining([ expect.objectContaining({ payload: expect.objectContaining({ cursor: null, limit: 50 }), }), expect.objectContaining({ payload: expect.objectContaining({ cursor: "2", limit: 50 }), }), ]), ); }); test("member people search starts at two characters", async ({ page }) => { const jmPubkey = "abababababababababababababababababababababababababababababababab"; await installMockBridge(page, { searchProfiles: [{ pubkey: jmPubkey, displayName: "jm" }], }); await page.goto("/"); await openMembersSidebar(page, "general"); await page.getByTestId("channel-management-search-users").fill("j"); await expect( page.getByTestId(`channel-user-search-result-${jmPubkey}`), ).toHaveCount(0); expect( (await readCommandPayloadLog(page)).filter( (entry) => entry.command === "search_users" && (entry.payload as { query?: string }).query === "j", ), ).toHaveLength(0); await page.getByTestId("channel-management-search-users").fill("jm"); await expect( page.getByTestId(`channel-user-search-result-${jmPubkey}`), ).toContainText("jm"); const searchCalls = (await readCommandPayloadLog(page)).filter( (entry) => entry.command === "search_users", ); expect(searchCalls).toEqual( expect.arrayContaining([ expect.objectContaining({ payload: expect.objectContaining({ query: "jm" }), }), ]), ); }); test("members modal does not show direct pubkey entry", async ({ page }) => { await page.goto("/"); await openMembersSidebar(page, "general"); await expect(page.getByTestId("channel-management-add-pubkeys")).toHaveCount( 0, ); await expect( page.getByTestId("channel-management-toggle-direct-pubkeys"), ).toHaveCount(0); await expect( page.getByTestId("channel-management-search-users"), ).toHaveAttribute("placeholder", "Add people and agents"); }); test("channel header omits the add agent action", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await page.setViewportSize({ width: 1280, height: 420 }); await expect(page.getByTestId("channel-add-bot-trigger")).toHaveCount(0); await expect(page.getByTestId("channel-members-trigger")).toBeVisible(); await expect(page.getByTestId("channel-start-huddle-trigger")).toBeVisible(); await expect(page.getByTestId("channel-management-trigger")).toBeVisible(); }); test("huddle rollback end event clears the active header action", async ({ page, }) => { await page.goto("/"); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); await waitForMockLiveSubscription(page, "random", KIND_HUDDLE_STARTED); const ephemeralChannelId = "10000000-0000-4000-8000-000000000001"; const createdAt = Math.floor(Date.now() / 1000); await page.evaluate( ({ createdAt, ephemeralChannelId, kind }) => { window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "random", content: JSON.stringify({ ephemeral_channel_id: ephemeralChannelId, }), createdAt, id: "1".repeat(64), kind, }); }, { createdAt, ephemeralChannelId, kind: KIND_HUDDLE_STARTED }, ); await expect( page.getByRole("button", { name: "Join active huddle (1 participant)", }), ).toBeVisible(); await page.evaluate( ({ createdAt, ephemeralChannelId, kind }) => { window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ channelName: "random", content: JSON.stringify({ ephemeral_channel_id: ephemeralChannelId, }), createdAt, id: "2".repeat(64), kind, }); }, { createdAt, ephemeralChannelId, kind: KIND_HUDDLE_ENDED }, ); await expect( page.getByTestId("channel-start-huddle-trigger"), ).toHaveAttribute("aria-label", "Start huddle"); }); test("channel header actions show tooltips", async ({ page }) => { await page.goto("/"); await page.getByTestId("channel-random").click(); await expect(page.getByTestId("chat-title")).toHaveText("random"); for (const [testId, label] of [ ["channel-members-trigger", "Channel members"], ["channel-huddle-tooltip-trigger", "Huddle"], ["channel-management-trigger", "Channel settings"], ] as const) { await page.getByTestId(testId).hover(); await expect(page.getByRole("tooltip", { name: label })).toBeVisible(); } }); test("members sidebar retains distinct same-persona managed agents", async ({ page, }) => { const inChannelAgentPubkey = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"; const outOfChannelAgentPubkey = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"; await installMockBridge(page, { managedAgents: [ { pubkey: outOfChannelAgentPubkey, name: "Pinky", personaId: "builtin:fizz", status: "stopped", }, { pubkey: inChannelAgentPubkey, name: "Pinky", personaId: "builtin:fizz", status: "running", channelNames: ["general"], }, ], searchProfiles: [ { pubkey: outOfChannelAgentPubkey, displayName: "Pinky", ownerPubkey: MOCK_IDENTITY_PUBKEY, isAgent: true, }, { pubkey: inChannelAgentPubkey, displayName: "Pinky", ownerPubkey: MOCK_IDENTITY_PUBKEY, isAgent: true, }, ], userSearchDelayMs: 1_000, }); await page.goto("/"); await openMembersSidebar(page, "general"); await page.getByTestId("channel-management-search-users").fill("pi"); await expect( page.getByTestId(`channel-user-search-result-${inChannelAgentPubkey}`), ).toHaveCount(0); await expect( page.getByTestId(`channel-user-search-result-${outOfChannelAgentPubkey}`), ).toBeVisible(); await expect(page.getByText("Pinky", { exact: true })).toHaveCount(2); }); test("private-channel members cannot add people without owner/admin", async ({ page, }) => { await installMockBridge(page, { managedAgents: [ { pubkey: TEST_IDENTITIES.charlie.pubkey, name: "charlie", status: "stopped", }, ], }); await page.goto("/"); // secret-projects is a private (non-DM) channel where the current user is a // plain member. The relay rejects their kind:9000, so the affordance is // withheld and the reason shown instead of failing after the fact. await openMembersSidebar(page, "secret-projects"); await expect(page.getByTestId("members-sidebar-add-denied")).toBeVisible(); // The field stays, but only as a filter over existing members. await expect( page.getByTestId("channel-management-search-users"), ).toHaveAttribute("placeholder", "Search people and agents"); await page.getByTestId("channel-management-search-users").fill("char"); await expect(page.getByText("Not in this channel")).toHaveCount(0); await expect( page.getByTestId( `channel-user-search-result-${TEST_IDENTITIES.charlie.pubkey}`, ), ).toHaveCount(0); await expect( page.getByTestId(`sidebar-member-${TEST_IDENTITIES.charlie.pubkey}`), ).toHaveCount(0); }); test("open-channel members can add people and managed agents without admin", async ({ page, }) => { await installMockBridge(page, { managedAgents: [ { pubkey: TEST_IDENTITIES.charlie.pubkey, name: "charlie", status: "stopped", }, ], }); await page.goto("/"); // random is open and the current user is a plain member there, so the // owner/admin requirement must not leak outside private channels. await openMembersSidebar(page, "random"); // The invite card is shown to any member of an open channel, not just // owners/admins. await expect( page.getByTestId("channel-management-search-users"), ).toBeVisible(); await expect(page.getByTestId("members-sidebar-add-denied")).toHaveCount(0); await page.getByTestId("channel-management-search-users").fill("char"); await page .getByTestId(`channel-user-search-result-${TEST_IDENTITIES.charlie.pubkey}`) .click(); await expect( page.getByTestId(`sidebar-member-${TEST_IDENTITIES.charlie.pubkey}`), ).toContainText("charlie"); // The modal no longer exposes member/guest/bot role choices or a staged // submit button; selected people are added immediately as members and // selected agents are added as bots. await expect(page.getByTestId("channel-management-add-role")).toHaveCount(0); await expect(page.getByTestId("channel-management-add-members")).toHaveCount( 0, ); }); test("removing a channel-scoped agent preserves the managed agent record", async ({ page, }) => { const agentName = `cleanup-agent-${Date.now()}`; await page.goto("/"); const agentPubkey = await addGenericAgent(page, "general", agentName); await page.getByTestId("channel-general").click(); await openMembersSidebar(page, "general"); await openMemberMenu(page, agentPubkey); await page.getByTestId(`sidebar-remove-member-${agentPubkey}`).click(); await expect(page.getByTestId(`sidebar-member-${agentPubkey}`)).toHaveCount( 0, ); await page.keyboard.press("Escape"); await expect(page.getByTestId("members-sidebar")).not.toBeVisible(); await page.getByTestId("open-agents-view").click(); await expect(page.getByTestId(`managed-agent-${agentPubkey}`)).toHaveCount(1); }); test("members sidebar can stop and start a managed bot in this community", async ({ page, }) => { const agentName = `sidebar-agent-${Date.now()}`; await page.goto("/"); const agentPubkey = await addGenericAgent(page, "general", agentName); const baselineCommands = await readCommandLog(page); const baselineLegacyStartCount = commandCount( baselineCommands, "start_managed_agent", ); const baselineLegacyStopCount = commandCount( baselineCommands, "stop_managed_agent", ); await openMembersSidebar(page, "general"); const agentStatus = page.getByTestId( `sidebar-managed-agent-status-${agentPubkey}`, ); const agentAction = page.getByTestId(`sidebar-agent-action-${agentPubkey}`); // The badge is pair-scoped: it reports the agent+active-community runtime. await expect(agentStatus).toContainText("Here"); await openMemberMenu(page, agentPubkey); await expect(agentAction).toContainText("Stop"); await agentAction.click(); await expect(agentStatus).toContainText("Unavailable"); await openMemberMenu(page, agentPubkey); await expect(agentAction).toContainText("Start"); await agentAction.click(); await expect(agentStatus).toContainText("Here"); await expect( page .locator("[data-sonner-toast]") .filter({ hasText: `Started ${agentName} in this community.` }), ).toBeVisible(); // The lifecycle menu item must dispatch the pair-scoped runtime commands, // never the legacy agent-wide start/stop that drains every community. const commands = await readCommandLog(page); expect(commandCount(commands, "stop_managed_agent_runtime")).toBe(1); expect(commandCount(commands, "start_managed_agent_runtime")).toBe(1); expect(commandCount(commands, "start_managed_agent")).toBe( baselineLegacyStartCount, ); expect(commandCount(commands, "stop_managed_agent")).toBe( baselineLegacyStopCount, ); }); test("stopping a managed bot in one community leaves its other communities running", async ({ page, }) => { const agentPubkey = TEST_IDENTITIES.charlie.pubkey; // Must match the relay of the community seeded by the mock bridge // (`seedDefaultCommunity` follows BUZZ_E2E_RELAY_URL); the agent also has a // live runtime in a second community on another relay. const activeRelayUrl = ( process.env.BUZZ_E2E_RELAY_URL ?? "http://localhost:3000" ).replace(/^http/, "ws"); const otherRelayUrl = "ws://other-community.example"; await installMockBridge(page, { managedAgents: [ { pubkey: agentPubkey, name: "charlie", status: "running", channelNames: ["general"], }, ], managedAgentRuntimes: [ { pubkey: agentPubkey, relayUrl: activeRelayUrl }, { pubkey: agentPubkey, relayUrl: otherRelayUrl }, ], }); await page.goto("/"); await openMembersSidebar(page, "general"); const agentStatus = page.getByTestId( `sidebar-managed-agent-status-${agentPubkey}`, ); const agentAction = page.getByTestId(`sidebar-agent-action-${agentPubkey}`); await expect(agentStatus).toContainText("Here"); await openMemberMenu(page, agentPubkey); await expect(agentAction).toContainText("Stop"); await agentAction.click(); await expect(agentStatus).toContainText("Unavailable"); await expect( page .locator("[data-sonner-toast]") .filter({ hasText: "Stopped charlie in this community." }), ).toBeVisible(); // Stop(A) must leave the community-B runtime untouched. const runtimes = await page.evaluate(async () => { const invoke = ( window as Window & { __BUZZ_E2E_INVOKE_MOCK_COMMAND__?: ( command: string, ) => Promise>; } ).__BUZZ_E2E_INVOKE_MOCK_COMMAND__; if (!invoke) { throw new Error("Mock bridge is not installed."); } return invoke("list_managed_agent_runtimes"); }); expect( runtimes.find((runtime) => runtime.relayUrl === activeRelayUrl)?.lifecycle, ).toBe("stopped"); expect( runtimes.find((runtime) => runtime.relayUrl === otherRelayUrl)?.lifecycle, ).toBe("ready"); // And it must never route through the legacy agent-wide stop. const commands = await readCommandLog(page); expect(commandCount(commands, "stop_managed_agent_runtime")).toBe(1); expect(commandCount(commands, "stop_managed_agent")).toBe(0); }); test("members sidebar omits bulk controls for managed bots", async ({ page, }) => { const firstAgentName = `sidebar-remove-a-${Date.now()}`; const secondAgentName = `sidebar-remove-b-${Date.now()}`; await page.goto("/"); const firstAgentPubkey = await addGenericAgent( page, "general", firstAgentName, ); const secondAgentPubkey = await addGenericAgent( page, "general", secondAgentName, ); await openMembersSidebar(page, "general"); await expect( page.getByTestId(`sidebar-member-${firstAgentPubkey}`), ).toBeVisible(); await expect( page.getByTestId(`sidebar-member-${secondAgentPubkey}`), ).toBeVisible(); await expect(page.getByTestId("members-sidebar-agent-controls")).toHaveCount( 0, ); await expect(page.getByTestId("members-sidebar-remove-all")).toHaveCount(0); await page.keyboard.press("Escape"); await expect(page.getByTestId("members-sidebar")).not.toBeVisible(); await page.getByTestId("open-agents-view").click(); await expect( page.getByTestId(`managed-agent-${firstAgentPubkey}`), ).toHaveCount(1); await expect( page.getByTestId(`managed-agent-${secondAgentPubkey}`), ).toHaveCount(1); const commands = await readCommandLog(page); expect( commands.filter((command) => command === "remove_channel_member"), ).toHaveLength(0); }); test("removing a multi-channel managed bot preserves its record after removal from all channels", async ({ page, }) => { const agentName = `multi-channel-agent-${Date.now()}`; const secondChannelName = `multi-home-${Date.now()}`; await page.goto("/"); const agentPubkey = await addGenericAgent(page, "general", agentName); await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(secondChannelName); await page .getByTestId("create-channel-description") .fill("Second home for managed bot cleanup coverage"); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("chat-title")).toHaveText(secondChannelName); const secondChannelId = await page .getByTestId(`channel-${secondChannelName}`) .getAttribute("data-channel-id"); if (!secondChannelId) { throw new Error("Second channel id missing."); } await invokeMockCommand(page, "add_channel_members", { channelId: secondChannelId, pubkeys: [agentPubkey], role: "bot", }); // Snapshot command counts before removals so assertions are relative. const baseline = await readCommandLog(page); const baselineRemoves = baseline.filter( (c) => c === "remove_channel_member", ).length; await openMembersSidebar(page, "general"); await openMemberMenu(page, agentPubkey); await page.getByTestId(`sidebar-remove-member-${agentPubkey}`).click(); await expect(page.getByTestId(`sidebar-member-${agentPubkey}`)).toHaveCount( 0, ); await page.keyboard.press("Escape"); await expect(page.getByTestId("members-sidebar")).not.toBeVisible(); await page.getByTestId("open-agents-view").click(); await expect(page.getByTestId(`managed-agent-${agentPubkey}`)).toHaveCount(1); let commands = await readCommandLog(page); // First removal: 1 remove_channel_member, agent record preserved. expect( commands.filter((c) => c === "remove_channel_member").length - baselineRemoves, ).toBe(1); await openMembersSidebar(page, secondChannelName); await openMemberMenu(page, agentPubkey); await page.getByTestId(`sidebar-remove-member-${agentPubkey}`).click(); await expect(page.getByTestId(`sidebar-member-${agentPubkey}`)).toHaveCount( 0, ); await page.keyboard.press("Escape"); await expect(page.getByTestId("members-sidebar")).not.toBeVisible(); await page.getByTestId("open-agents-view").click(); await expect(page.getByTestId(`managed-agent-${agentPubkey}`)).toHaveCount(1); commands = await readCommandLog(page); // Second removal: agent is preserved even after removal from all channels. expect( commands.filter((c) => c === "remove_channel_member").length - baselineRemoves, ).toBe(2); }); test("bulk remove stays hidden when row-level remove is not allowed", async ({ page, }) => { const alicePubkey = "953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f"; await page.goto("/"); // Join the "design" channel (unjoined by default) via the channel browser. // The user becomes a regular member — not admin/owner. await openChannelBrowser(page); await expect(page.getByTestId("channel-browser-dialog")).toBeVisible(); await page .getByTestId("browse-channel-design") .getByRole("button", { name: "Join" }) .click(); await expect(page.getByTestId("chat-title")).toHaveText("design"); await openMembersSidebar(page, "design"); // Alice is a relay-observed bot in design (present in mockRelayAgents) that // the user does not manage locally. Since there is no local managed agent // for alice, hasActions is false and no 3-dot menu renders. await expect(page.getByTestId(`sidebar-member-${alicePubkey}`)).toBeVisible(); await expect( page.getByTestId(`sidebar-member-menu-${alicePubkey}`), ).toHaveCount(0); // Bulk agent controls only render when hasControllableManagedBots is true. // Since no bots in design have a local managed agent, the controls are absent. await expect(page.getByTestId("members-sidebar-agent-controls")).toHaveCount( 0, ); }); test("open channel management supports join and leave", async ({ page }) => { await page.goto("/"); // Navigate to "design" (an unjoined channel) via the channel browser await openChannelBrowser(page); await expect(page.getByTestId("channel-browser-dialog")).toBeVisible(); await page .getByTestId("browse-channel-design") .getByRole("button", { name: "Join" }) .click(); await expect(page.getByTestId("chat-title")).toHaveText("design"); // Open members sidebar — should show current user after joining await page.getByTestId("channel-members-trigger").click(); await expect(page.getByTestId("members-sidebar")).toBeVisible(); await expect( page.getByTestId(`sidebar-member-${MOCK_IDENTITY_PUBKEY}`), ).toContainText("You"); await page.keyboard.press("Escape"); // Open channel management — should show Leave since we just joined await page.getByTestId("channel-management-trigger").click(); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); await expect(page.getByTestId("channel-management-join")).toHaveCount(0); await expect(page.getByTestId("channel-management-leave")).toBeVisible(); // Leave the channel await page.getByTestId("channel-management-leave").click(); await expect(page.getByTestId("channel-management-sheet")).not.toBeVisible(); // After leaving, the app navigates away — re-open browser and find design await openChannelBrowser(page); await expect(page.getByTestId("channel-browser-dialog")).toBeVisible(); // "design" should be back in the unjoined section with a Join button await expect( page .getByTestId("browse-channel-design") .getByRole("button", { name: "Join" }), ).toBeVisible(); }); test("manage channel can archive and unarchive a stream", async ({ page }) => { await page.goto("/"); await openChannelManagement(page, "general"); await page.getByTestId("channel-management-archive").click(); await expect(page.getByTestId("channel-management-unarchive")).toBeVisible(); await closeChannelManagement(page); await expect(page.getByTestId("stream-list")).not.toContainText("general"); await expect(page.getByTestId("message-input")).toHaveAttribute( "contenteditable", "false", ); await expect(page.getByTestId("send-message")).toBeDisabled(); await openChannelBrowser(page); await expect(page.getByTestId("channel-browser-dialog")).toBeVisible(); await expect(page.getByTestId("browse-channel-general")).toContainText( "archived", ); await page.getByTestId("browse-channel-general").click(); await expect(page.getByTestId("channel-browser-dialog")).not.toBeVisible(); await expect(page.getByTestId("chat-title")).toHaveText("general"); await page.getByTestId("channel-management-trigger").click(); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); await page.getByTestId("channel-management-unarchive").click(); await expect(page.getByTestId("channel-management-archive")).toBeVisible(); await closeChannelManagement(page); await expect(page.getByTestId("stream-list")).toContainText("general"); await expect(page.getByTestId("message-input")).toHaveAttribute( "contenteditable", "true", ); }); test("manage channel can delete an owned stream", async ({ page }) => { const channelName = `delete-me-${Date.now()}`; await page.goto("/"); await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(channelName); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); await page.getByTestId("channel-management-trigger").click(); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); await page.getByTestId("channel-management-delete").click(); await expect( page.getByTestId("channel-delete-confirmation-dialog"), ).toBeVisible(); await page.getByTestId("channel-delete-confirm").click(); await expect(page.getByTestId("home-inbox-list")).toBeVisible(); await expect(page.getByTestId("stream-list")).not.toContainText(channelName); }); test("canceling channel deletion keeps the owned stream", async ({ page }) => { const channelName = `keep-me-${Date.now()}`; await page.goto("/"); await openCreateChannelDialog(page); await page.getByTestId("create-channel-name").fill(channelName); await page.getByTestId("create-channel-submit").click(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); await page.getByTestId("channel-management-trigger").click(); await expect(page.getByTestId("channel-management-sheet")).toBeVisible(); await page.getByTestId("channel-management-delete").click(); await expect( page.getByTestId("channel-delete-confirmation-dialog"), ).toBeVisible(); await page.getByTestId("channel-delete-cancel").click(); await expect( page.getByTestId("channel-delete-confirmation-dialog"), ).not.toBeVisible(); await expect(page.getByTestId("chat-title")).toHaveText(channelName); await expect(page.getByTestId("stream-list")).toContainText(channelName); });