Files
cls 9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
feat: import Chinese-localized Buzz source snapshot
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2026-08-13 18:34:25 +08:00

4338 lines
143 KiB
TypeScript

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<string> {
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<string, unknown>,
) => 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<void>;
};
}
).__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<T>(
page: import("@playwright/test").Page,
command: string,
payload?: Record<string, unknown>,
) {
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<string, unknown>;
}) => {
const invoke = (
window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
}
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) {
throw new Error("Mock bridge is not installed.");
}
return invoke(command, payload);
},
{ command, payload },
) as Promise<T>;
}
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<typeof box> => 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<HTMLElement>("[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<string, unknown>,
) => 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<HTMLElement>(
'[data-testid="message-timeline-sticky-day-divider"]',
);
const pinnedPill = pinnedDivider?.querySelector<HTMLElement>(
'[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<HTMLElement>(
'[data-testid="message-timeline-day-group"]',
),
].find((candidate) => {
const pill = candidate.querySelector<HTMLElement>("p");
return pill && getComputedStyle(pill).visibility === "visible";
});
const pill = group?.querySelector<HTMLElement>("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<Array<{ relayUrl: string; lifecycle: string }>>;
}
).__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);
});