Files
buzz/desktop/tests/e2e/mentions.spec.ts
Taylor Ho 8dbc65d9e2 fix(composer): polish automatic mentions (#6956)
**Category:** improvement
**User Impact:** Automatic mentions are easier to turn off and now
behave consistently across conversations, settings, drafts, and repeated
agent mentions.

**Problem:** People found the new automatic mention behavior hard to
control: turning it off in Settings did not reliably affect the
composer, removing a mention could require also disabling the feature,
and root/thread composers could inherit or restore surprising state.
Other reported rough edges included only one of several mentioned agents
becoming automatic, synthetic mentions leaking into drafts, restored
mentions corrupting adjacent text, controls remaining visible in
archived channels, and unclear picker feedback. See the [original
feedback
thread](buzz://message?channel=e62570dd-33ad-42c5-b92b-75f2689f9694&id=c949ec399274fbb0d6633da3f95712e843a67dcada214f63d72f6975b406604b).

**Solution:** Polish the existing feature around the problems people
encountered, keeping automatic mentions controllable and scoped to the
active conversation.

| Reported issue | UX fix |
| --- | --- |
| Turning automatic mentions off in Settings did not reliably update the
composer. | The global setting and composer control stay synchronized,
and disabling the feature does not clear typed text. |
| Removing an automatic mention could require both deleting the mention
and turning off the feature. | Removing or unchecking an agent excludes
that agent for the current conversation, while explicitly re-adding the
agent can restore automatic mention behavior. |
| Root and thread composers could share or restore surprising
selections. | Each root or thread composer keeps its own automatic
audience and restores it when the user returns. A request to enable
automatic mentions only in agent threads was considered; this PR keeps
them available at the channel root but prevents state from leaking
between the two. |
| Mentioning multiple agents could leave only one saved as automatic. |
Multi-agent selections remain represented in the automatic audience and
restored mention chips. |
| Automatic mention prefixes could be saved as if the user typed them. |
Synthetic prefixes stay out of persisted drafts while authored text is
preserved. |
| Restored mentions could lose their separator and corrupt continued
typing. | Restored multi-word mentions retain their trailing space and
place the caret after it. |
| Archived channels showed automatic-mention state beside a disabled
composer. | Disabled composers hide automatic-mention controls while
preserving the draft and restoring state when re-enabled. |
| Confirmation and picker behavior made the feature feel difficult to
inspect or adjust. | Confirmations dismiss with removed agents, remain
open while hovered, and expose the setting before it changes; pin icons,
contrast, scope copy, animation, and keyboard toggling are also
clarified. |
| Agent suggestions and membership state could shift during directory
refreshes. | Suggestions and membership labels stay stable during
refreshes, while send-time authorization still revalidates access. |

## Changes

<details>
<summary>File changes</summary>


**desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs**
Adds coverage for the channel-roster eligibility rules used by agent
mention autocomplete.

**desktop/src/features/agents/lib/agentAutocompleteEligibility.ts**
Aligns agent autocomplete eligibility with channel membership so
available agents and their labels stay trustworthy.

**desktop/src/features/channels/ui/MembersSidebar.tsx**
Uses the shared member-pubkey logic when presenting and acting on
channel members.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.test.mjs**
Covers preference changes that must remain stable while composer
controls are toggled.


**desktop/src/features/messages/lib/autoPinMentionedAgentsPreference.ts**
Keeps the automatic-mention preference as durable user intent rather
than transient composer state.

**desktop/src/features/messages/lib/mentionMemberPubkeys.ts**
Centralizes which member identities count as mentionable in the current
channel.

**desktop/src/features/messages/lib/persistentAgentAudience.test.mjs**
Expands lifecycle coverage for persistent agent audiences, explicit
exclusions, and restored mentions.

**desktop/src/features/messages/lib/persistentAgentAudience.ts**
Models automatic, explicit, and excluded agent audiences separately so
user choices survive updates without leaking across composers.


**desktop/src/features/messages/lib/stripImplicitAgentMentions.test.mjs**
Verifies implicit automatic mentions are removed without damaging
surrounding separators or authored content.

**desktop/src/features/messages/lib/stripImplicitAgentMentions.ts**
Strips presentation-only automatic mentions before draft persistence
while preserving whitespace and authored text.

**desktop/src/features/messages/lib/useMentions.ts**
Routes mention insertion and removal through the composer-local audience
lifecycle.

**desktop/src/features/messages/lib/useRichTextEditor.ts**
Preserves mention-chip structure and caret placement when automatic
mentions are restored.

**desktop/src/features/messages/ui/ComposerAddressControls.test.mjs**
Updates control-state expectations for disabled automatic mentions and
restored pin affordances.

**desktop/src/features/messages/ui/ComposerAddressControls.tsx**
Makes automatic-mention state, disabled presentation, and pin controls
visually explicit.

**desktop/src/features/messages/ui/MentionAutocomplete.test.mjs**
Adds coverage for roster labels, pin state, and picker behavior after
mention selection.

**desktop/src/features/messages/ui/MentionAutocomplete.tsx**
Keeps the shortcut picker open for repeated selection and restores
visible automatic-mention pin indicators.

**desktop/src/features/messages/ui/MessageComposer.tsx**
Scopes automatic mention state to each root or thread composer and
coordinates restoration, draft persistence, and sending.

**desktop/src/features/messages/ui/MessageComposerToolbar.tsx**
Passes the effective automatic-mention state into the toolbar
presentation.

**desktop/src/features/messages/ui/composerAgentKeyboard.test.mjs**
Updates keyboard interaction coverage for toggling agents in place.

**desktop/src/features/messages/ui/useAddressedAgentMentionRestore.ts**
Restores automatic mention chips after lifecycle changes without moving
or duplicating authored content.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.test.mjs**
Substantially expands coverage for toggles, exclusions, synchronization,
and picker dismissal rules.

**desktop/src/features/messages/ui/useAgentAddressLockPicker.ts**
Keeps the picker usable across repeated choices and preserves explicit
per-agent intent while settings change.

**desktop/src/features/messages/ui/useAlwaysAddressShortcut.ts**
Makes the keyboard shortcut toggle the highlighted automatic audience
choice without replacing unrelated selections.

**desktop/src/features/messages/ui/useAutoPinMentionedAgents.ts**
Owns composer-local automatic-mention lifecycle behavior, including
restoration, exclusions, deletion, and disabled-state handling.

**desktop/src/features/messages/ui/useComposerMentionPicker.test.mjs**
Adds focused picker lifecycle coverage for selection, hover, and
dismissal behavior.

**desktop/src/features/messages/ui/useComposerMentionPicker.ts**
Prevents premature picker dismissal while the user is interacting with
its controls.

**desktop/src/features/messages/ui/useDraftPersistSnapshot.ts**
Persists only user-authored draft content rather than implicit automatic
mention decorations.

**desktop/src/shared/lib/keyboard-shortcuts.ts**
Updates the automatic-mention shortcut description to match its toggle
behavior.

**desktop/src/testing/e2eBridge.ts**
Extends the desktop test bridge with the state needed to exercise roster
and automatic-mention transitions.

**desktop/tests/e2e/mentions.spec.ts**
Covers roster-based labels, managed-agent invitation, revocation, and
recovery behavior in the complete mention flow.

**desktop/tests/e2e/persistent-agent-audience.spec.ts**
Adds end-to-end coverage for root/thread isolation, preference
synchronization, manual exclusions, draft hygiene, restored chips,
separators, hover behavior, and disabled presentation.

</details>

## Reproduction Steps

1. Open a channel with at least two available agents and enable
automatic mentions from the composer mention control.
2. Select multiple agents, remove or uncheck one, and confirm subsequent
composer updates keep that agent excluded while the others remain
automatic.
3. Open a thread, choose a different automatic audience there, and
switch between the thread and root composer; confirm each composer
retains only its own choices.
4. Disable automatic mentions and confirm the draft text remains
unchanged while automatic chips and controls show the disabled state;
re-enable the setting and confirm eligible automatic chips return.
5. Delete an automatic mention chip, then explicitly add the agent
again; confirm it immediately returns as an automatic mention without
disturbing spaces or the caret, including for a multi-word name.
6. Reload with a saved draft and confirm implicit automatic mentions
were not persisted as authored draft text.
7. Use the automatic-mention keyboard shortcut and picker repeatedly;
confirm the picker remains open for additional choices and the
highlighted agent toggles in place.

## Validation

Validated at `34d208b47d64a9816f88e10a46bcfd479e917d75` after rebasing
onto `origin/main` (`69096c9a8`):

- Desktop unit tests: 5,731 passed, 0 failed.
- Desktop TypeScript typecheck: passed.
- Desktop E2E build: passed; emitted only existing chunk and
dynamic-import warnings.
- `pnpm check`: exited successfully; 4 warnings and 5 informational
findings are in unrelated files introduced by current main.

## Screenshots/Demos

The behavioral changes are covered by the focused desktop E2E scenarios
above. Screenshots can be attached from the screenshot-producing
automatic-mention E2E after the PR is created.

---------

Signed-off-by: Taylor Ho <taylorkmho@gmail.com>
Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
Co-authored-by: Carl <acda9e433d19dcd0e6b6840f7f4b98f3a56f1fab98049d444c087019e6d36560@buzz.block.builderlab.xyz>
Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz>
Co-authored-by: morgmart <98432065+morgmart@users.noreply.github.com>
2026-08-28 17:11:04 -07:00

3666 lines
115 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import {
installMockBridge,
openChannelBrowser,
TEST_IDENTITIES,
} from "../helpers/bridge";
const MOCK_VIEWER_PUBKEY = "deadbeef".repeat(8);
const GENERAL_CHANNEL_ID = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
const IN_CHANNEL_MANAGED_AGENT_PUBKEY =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const OUT_OF_CHANNEL_MANAGED_AGENT_PUBKEY =
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
const OUT_OF_CHANNEL_PROVIDER_AGENT_PUBKEY =
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
const REUSABLE_PERSONA_AGENT_PUBKEY =
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
const ALLOWLIST_RELAY_AGENT_PUBKEY =
"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee";
const DELAYED_RELAY_AGENT_PUBKEY =
"9999999999999999999999999999999999999999999999999999999999999999";
const CASEY_PROFILE_PUBKEY =
"1111111111111111111111111111111111111111111111111111111111111111";
const PROFILE_ONLY_AGENT_PUBKEY =
"8f83d6b7f3d74f7d933ae3a54dd8c6cc85c7f98e531c16e5a827b953441a8d67";
const OWNED_AGENT_PROFILE_PUBKEY =
"1212121212121212121212121212121212121212121212121212121212121212";
const SYSTEM_MESSAGE_KIND = 40099;
const DM_THREAD_AGENT_MENTION_ERROR_TEXT =
"Agents must already be in a DM to be mentioned in its threads. Start a new conversation that includes the agent.";
const DM_THREAD_MEMBERS_LOADING_ERROR_TEXT =
"Checking conversation members. Try again in a moment.";
/** Locator scoped to the mention autocomplete dropdown inside the composer. */
function autocomplete(page: import("@playwright/test").Page) {
return page
.getByTestId("message-composer")
.getByTestId("mention-autocomplete");
}
async function readCommandLog(page: import("@playwright/test").Page) {
return page.evaluate(() => {
return (
(window as Window & { __BUZZ_E2E_COMMANDS__?: string[] })
.__BUZZ_E2E_COMMANDS__ ?? []
);
});
}
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 readOutgoingMentionPubkeys(
page: import("@playwright/test").Page,
content: string,
) {
return page.evaluate((expectedContent) => {
const signedEvent = (
window as Window & {
__BUZZ_E2E_SIGNED_EVENTS__?: Array<{
content?: string;
tags?: string[][];
}>;
}
).__BUZZ_E2E_SIGNED_EVENTS__?.find(
(event) => event.content === expectedContent,
);
if (signedEvent) {
return (signedEvent.tags ?? [])
.filter((tag) => tag[0] === "p" && tag[1])
.map((tag) => tag[1]);
}
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 === "send_channel_message") {
const payload = entry.payload as
| { content?: string; mentionPubkeys?: string[] }
| undefined;
if (payload?.content === expectedContent) {
return payload.mentionPubkeys ?? [];
}
}
if (entry.command === "sign_event") {
const unsignedEvent = entry.payload as
| { content?: string; tags?: string[][] }
| undefined;
if (unsignedEvent?.content !== expectedContent) continue;
return (unsignedEvent.tags ?? [])
.filter((tag) => tag[0] === "p" && tag[1])
.map((tag) => tag[1]);
}
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 !== expectedContent) {
continue;
}
return (frame[1].tags ?? [])
.filter((tag) => tag[0] === "p" && tag[1])
.map((tag) => tag[1]);
} catch {}
}
return null;
}, content);
}
function commandCount(commands: string[], command: string) {
return commands.filter((entry) => entry === command).length;
}
async function emitMockMessage(
page: import("@playwright/test").Page,
channelName: string,
content: string,
options?: {
kind?: number;
mentionPubkeys?: string[];
parentEventId?: string;
pubkey?: string;
},
) {
const event = await page.evaluate(
({ ch, kind, mentionPubkeys, msg, parentEventId, pubkey }) => {
return (
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
kind?: number;
mentionPubkeys?: string[];
parentEventId?: string | null;
pubkey?: string;
}) => { id: string; created_at: number; pubkey: string };
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: ch,
content: msg,
kind,
mentionPubkeys,
parentEventId: parentEventId ?? undefined,
pubkey: pubkey ?? undefined,
});
},
{
ch: channelName,
kind: options?.kind,
mentionPubkeys: options?.mentionPubkeys,
msg: content,
parentEventId: options?.parentEventId ?? null,
pubkey: options?.pubkey ?? TEST_IDENTITIES.alice.pubkey,
},
);
if (!event) {
throw new Error("Mock message emitter is not installed");
}
return event;
}
async function waitForMockLiveSubscription(
page: import("@playwright/test").Page,
channelName: string,
kind?: number,
) {
await expect
.poll(async () => {
return page.evaluate(
({ currentChannelName, kind: expectedKind }) => {
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: expectedKind,
}) ?? false
);
},
{ currentChannelName: channelName, kind },
);
})
.toBe(true);
}
// The channel timeline renders off a `useDeferredValue` snapshot that lags the
// latest `messages` by a commit; the list wrapper carries
// `data-render-pending="true"` while that commit is in flight and drops the
// attribute once it settles. Poll for its absence before asserting on
// freshly-sent content so the assertion does not race the deferred commit.
async function waitForTimelineSettled(page: import("@playwright/test").Page) {
await expect(page.locator("[data-render-pending]")).toHaveCount(0);
}
async function expectOwnedAgentProfileActions(
profilePopover: import("@playwright/test").Locator,
pubkey: string,
) {
await expect(
profilePopover.getByTestId(`user-profile-popover-message-${pubkey}`),
).toBeVisible();
await expect(
profilePopover.getByTestId(`user-profile-popover-wave-${pubkey}`),
).toHaveCount(0);
await expect(
profilePopover.getByTestId(`user-profile-popover-huddle-${pubkey}`),
).toBeVisible();
}
async function expectAgentProfileActionsHidden(
profilePopover: import("@playwright/test").Locator,
pubkey: string,
) {
await expect(
profilePopover.getByTestId(`user-profile-popover-message-${pubkey}`),
).toHaveCount(0);
await expect(
profilePopover.getByTestId(`user-profile-popover-wave-${pubkey}`),
).toHaveCount(0);
await expect(
profilePopover.getByTestId(`user-profile-popover-huddle-${pubkey}`),
).toHaveCount(0);
}
test("@ trigger prioritizes channel members before runnable personas and other managed agents", async ({
page,
}) => {
await installMockBridge(page, {
activePersonaIds: ["builtin:fizz"],
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@");
const dropdown = autocomplete(page);
await expect(dropdown).toBeVisible();
await expect(dropdown.getByText("alice")).toBeVisible();
await expect(dropdown.getByText("bob")).toBeVisible();
await expect(dropdown.getByText("Fizz")).toBeVisible();
await expect(dropdown.getByText("charlie")).toBeVisible();
await expect(dropdown.getByText("outsider")).toHaveCount(0);
const charlieRow = dropdown.locator("button", { hasText: "charlie" });
await expect(charlieRow.getByTestId("mention-agent-icon")).toBeVisible();
await expect(charlieRow.getByText("not in channel")).toBeVisible();
await expect(
dropdown
.locator("button", { hasText: "alice" })
.getByText("not in channel"),
).not.toBeVisible();
const suggestions = dropdown.locator("button");
const suggestionText = await suggestions.allInnerTexts();
const aliceIndex = suggestionText.findIndex((text) => text.includes("alice"));
const fizzIndex = suggestionText.findIndex((text) => text.includes("Fizz"));
const bobIndex = suggestionText.findIndex((text) => text.includes("bob"));
const charlieIndex = suggestionText.findIndex((text) =>
text.includes("charlie"),
);
const outsiderIndex = suggestionText.findIndex((text) =>
text.includes("outsider"),
);
expect(aliceIndex).toBeGreaterThanOrEqual(0);
expect(fizzIndex).toBeGreaterThanOrEqual(0);
expect(bobIndex).toBeGreaterThanOrEqual(0);
expect(charlieIndex).toBeGreaterThanOrEqual(0);
expect(outsiderIndex).toEqual(-1);
expect(aliceIndex).toBeLessThan(fizzIndex);
expect(bobIndex).toBeLessThan(fizzIndex);
expect(fizzIndex).toBeLessThan(charlieIndex);
});
test("duplicate owned agents preserve provenance and exact pubkey selection", async ({
page,
}) => {
const managedPubkey = IN_CHANNEL_MANAGED_AGENT_PUBKEY;
const relayPubkey = ALLOWLIST_RELAY_AGENT_PUBKEY;
await installMockBridge(page, {
managedAgents: [
{
pubkey: managedPubkey,
name: "carl",
status: "running",
channelNames: ["general"],
backend: {
type: "provider",
id: "mock",
config: {},
},
},
],
relayAgents: [
{
pubkey: relayPubkey,
ownerPubkey: MOCK_VIEWER_PUBKEY,
name: "carl",
respondTo: "owner-only",
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.evaluate(
async ({ channelId, pubkey }) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock bridge is not installed.");
await invoke("add_channel_members", {
channelId,
pubkeys: [pubkey],
role: "bot",
});
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
});
},
{ channelId: GENERAL_CHANNEL_ID, pubkey: relayPubkey },
);
const input = page.getByTestId("message-input");
await input.fill("@carl");
const dropdown = autocomplete(page);
const managedRow = dropdown.getByTestId(
`mention-suggestion-${managedPubkey}`,
);
const relayRow = dropdown.getByTestId(`mention-suggestion-${relayPubkey}`);
await expect(managedRow).toContainText("agent");
await expect(managedRow.getByTestId("mention-agent-provenance")).toHaveCount(
0,
);
await expect(relayRow).toContainText("agent");
const relayProvenanceMarker = relayRow.getByTestId(
"mention-agent-provenance",
);
await expect(relayProvenanceMarker).toHaveAttribute(
"aria-label",
"From another Buzz setup",
);
await expect(relayProvenanceMarker).toHaveAttribute(
"title",
"From another Buzz setup",
);
await expect(relayProvenanceMarker).toBeVisible();
await expect(relayProvenanceMarker).toHaveText("");
await expect(relayProvenanceMarker.locator("svg")).toBeVisible();
await expect(managedRow).not.toContainText("managed by you");
await expect(relayRow).not.toContainText("managed by you");
await page.setViewportSize({ width: 760, height: 640 });
await expect(relayProvenanceMarker).toBeVisible();
const rowBox = await relayRow.boundingBox();
const dropdownBox = await dropdown.boundingBox();
expect(rowBox).not.toBeNull();
expect(dropdownBox).not.toBeNull();
expect((rowBox?.x ?? 0) + (rowBox?.width ?? 0)).toBeLessThanOrEqual(
(dropdownBox?.x ?? 0) + (dropdownBox?.width ?? 0) + 1,
);
const collisionKeys = dropdown.getByTestId("mention-collision-npub");
await expect(collisionKeys).toHaveCount(2);
const fullNpubs = await collisionKeys.evaluateAll((nodes) =>
nodes.map((node) => node.getAttribute("title")),
);
expect(fullNpubs).toHaveLength(2);
expect(new Set(fullNpubs).size).toBe(2);
await managedRow
.getByRole("button", { name: "Automatically mention carl", exact: true })
.click();
await expect(
page.getByTestId(`composer-address-lock-${managedPubkey}`),
).toBeVisible();
await expect(
page.getByTestId(`composer-address-lock-${relayPubkey}`),
).toHaveCount(0);
await input.pressSequentially("local");
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@carl local"))
.toEqual([managedPubkey]);
await expect(input).toHaveText("@carl ");
await page.getByTestId(`composer-address-lock-${managedPubkey}`).click();
await input.fill("@carl");
const reopenedDropdown = autocomplete(page);
await expect(reopenedDropdown).toBeVisible();
const reopenedRelayRow = reopenedDropdown.getByTestId(
`mention-suggestion-${relayPubkey}`,
);
await reopenedRelayRow
.getByRole("button", { name: "Automatically mention carl", exact: true })
.click();
await expect(
page.getByTestId(`composer-address-lock-${relayPubkey}`),
).toBeVisible();
await expect(
page.getByTestId(`composer-address-lock-${managedPubkey}`),
).toHaveCount(0);
await input.pressSequentially("remote");
await page.getByTestId("send-message").click();
const sendWithoutInviting = page.getByRole("button", { name: "Do nothing" });
try {
await sendWithoutInviting.waitFor({ state: "visible", timeout: 2_000 });
await sendWithoutInviting.click();
} catch {
// In-channel selections send immediately without opening the prompt.
}
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@carl remote"))
.toEqual([relayPubkey]);
await page.getByTestId("channel-members-trigger").click();
const remoteSidebarMarker = page.getByTestId(
`sidebar-member-agent-provenance-${relayPubkey}`,
);
await expect(
page.getByTestId(`sidebar-member-agent-provenance-${managedPubkey}`),
).toHaveCount(0);
await expect(remoteSidebarMarker).toHaveAttribute(
"aria-label",
"From another Buzz setup",
);
await expect(remoteSidebarMarker).toHaveText("");
await expect(remoteSidebarMarker.locator("svg")).toBeVisible();
const remoteSidebarRow = page.getByTestId(`sidebar-member-${relayPubkey}`);
const localSidebarMarker = page.getByTestId(
`sidebar-member-agent-provenance-${managedPubkey}`,
);
const markerRemainsVisible = () =>
remoteSidebarMarker.evaluate((marker) => {
let element: Element | null = marker;
while (element) {
const style = window.getComputedStyle(element);
if (style.display === "none" || style.visibility === "hidden") {
return false;
}
if (Number(style.opacity) === 0) return false;
element = element.parentElement;
}
return true;
});
await expect.poll(markerRemainsVisible).toBe(true);
await expect(localSidebarMarker).toHaveCount(0);
const sidebarMarkerBox = await remoteSidebarMarker.boundingBox();
const sidebarBox = await page.getByTestId("members-sidebar").boundingBox();
expect(sidebarMarkerBox).not.toBeNull();
expect(sidebarBox).not.toBeNull();
expect(
(sidebarMarkerBox?.x ?? 0) + (sidebarMarkerBox?.width ?? 0),
).toBeLessThanOrEqual((sidebarBox?.x ?? 0) + (sidebarBox?.width ?? 0) + 1);
await remoteSidebarRow.hover();
await page.waitForTimeout(200);
await expect.poll(markerRemainsVisible).toBe(true);
await expect(localSidebarMarker).toHaveCount(0);
await page.getByTestId(`sidebar-member-open-profile-${relayPubkey}`).focus();
await page.waitForTimeout(200);
await expect.poll(markerRemainsVisible).toBe(true);
await expect(localSidebarMarker).toHaveCount(0);
});
test("relay-only shared agents emit an outbound mention tag when selected", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Ask @alice");
const aliceRow = autocomplete(page).locator("button", { hasText: "alice" });
await expect(aliceRow).toBeVisible();
await aliceRow.click();
await page.keyboard.type("please reply");
const content = "Ask @alice please reply";
await expect(input).toHaveText(content);
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, content))
.toContain(TEST_IDENTITIES.alice.pubkey);
});
test("typing an exact agent name and Space commits its chip and mention tag", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@alice");
await expect(
autocomplete(page).getByTestId(
`mention-suggestion-${TEST_IDENTITIES.alice.pubkey}`,
),
).toBeVisible();
await input.fill("Ask @alice");
await input.press(" ");
await page.keyboard.type("please reply");
const content = "Ask @alice please reply";
await expect(input).toHaveText(content);
await expect(
input.locator(".agent-mention-highlight", { hasText: "alice" }),
).toBeVisible();
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, content))
.toContain(TEST_IDENTITIES.alice.pubkey);
});
test("Shift+Space leaves an exact agent name plain and emits no mention tag", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: OUT_OF_CHANNEL_MANAGED_AGENT_PUBKEY,
name: "quinn",
status: "stopped",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
// Plain in-channel member names are intentionally tagged at send time, so
// use an authorized non-member to isolate selection from text extraction.
const input = page.getByTestId("message-input");
await input.fill("@quinn");
await expect(
autocomplete(page).getByTestId(
`mention-suggestion-${OUT_OF_CHANNEL_MANAGED_AGENT_PUBKEY}`,
),
).toBeVisible();
await input.fill("Ask @quinn");
await input.press("Shift+Space");
await page.keyboard.type("please reply");
const content = "Ask @quinn please reply";
await expect(input).toHaveText(content);
await expect(input.locator(".agent-mention-highlight")).toHaveCount(0);
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, content))
.toEqual([]);
});
// The three tests below pin the code-context gate on the Space commit. They
// key off casing, because a commit rewrites the draft to the candidate's
// canonical display name: a surviving "@ALICE" means the typed text was left
// alone. Chip decorations are deliberately not asserted — they already render
// over known names inside code, which is a separate pre-existing gap.
test("Space inside a code block leaves an exact agent name literal", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.click();
await page.keyboard.type("```");
await page.keyboard.press("Enter");
await expect(input.locator("pre")).toBeVisible();
await page.keyboard.type("deploy @ALICE");
await page.keyboard.press(" ");
await page.keyboard.type("now");
await expect(input.locator("pre")).toHaveText("deploy @ALICE now");
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "```\ndeploy @ALICE now\n```"))
.toEqual([]);
});
test("Space inside an inline code span leaves an exact agent name literal", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.click();
// The closing backtick turns the span into a code mark, which drops the
// backticks from the text the mention pipeline reads.
await page.keyboard.type("run `@ALICE`");
await expect(input.locator("code")).toHaveText("@ALICE");
await page.keyboard.press(" ");
await page.keyboard.type("now");
await expect(input.locator("code")).toHaveText("@ALICE");
await expect(input).toHaveText("run @ALICE now");
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "run `@ALICE` now"))
.toEqual([]);
});
test("Space still resolves an exact agent name typed after a code span", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.click();
await page.keyboard.type("run `deploy` @ALICE");
await page.keyboard.press(" ");
await page.keyboard.type("now");
const content = "run `deploy` @alice now";
await expect(input).toHaveText("run deploy @alice now");
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, content))
.toContain(TEST_IDENTITIES.alice.pubkey);
});
test("thread autocomplete keeps multiple long names readable in a narrow panel", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey:
"9999999999999999999999999999999999999999999999999999999999999999",
name: "Brain With A Very Long Name",
status: "stopped",
},
{
pubkey:
"9999999999999999999999999999999999999999999999999999999999999998",
name: "Brainstorming Assistant With A Long Name",
status: "stopped",
},
{
pubkey:
"9999999999999999999999999999999999999999999999999999999999999997",
name: "Brainy Helper With Another Long Name",
status: "stopped",
},
],
});
await page.setViewportSize({ width: 900, height: 640 });
await page.addInitScript(() => {
window.sessionStorage.setItem("buzz.desktop.thread-panel-width", "300");
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.setViewportSize({ width: 760, height: 640 });
await emitMockMessage(page, "general", "Reply to open the thread", {
parentEventId: "mock-general-welcome",
});
const threadSummary = page.getByTestId("message-thread-summary").first();
await expect(threadSummary).toBeVisible();
await threadSummary.click();
const threadPanel = page.getByTestId("message-thread-panel");
await expect(threadPanel).toBeVisible();
const panelBox = await threadPanel.boundingBox();
expect(panelBox?.width ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual(320);
const input = threadPanel.getByTestId("message-input");
await input.fill("@Brain");
const dropdown = threadPanel.getByTestId("mention-autocomplete");
await expect(dropdown).toBeVisible();
for (const name of [
"Brain With A Very Long Name",
"Brainstorming Assistant With A Long Name",
"Brainy Helper With Another Long Name",
]) {
const row = dropdown.locator("button", { hasText: name });
await expect(row).toBeVisible();
await expect(
row.getByTestId("mention-suggestion-avatar-fallback"),
).toBeVisible();
await expect(row.getByText("agent")).toBeVisible();
await expect(row.getByText("managed by you")).toBeVisible();
await expect(row.getByText(name)).not.toHaveCSS(
"text-overflow",
"ellipsis",
);
}
});
test("blocks non-participant persona mentions in DM threads", async ({
page,
}) => {
await installMockBridge(page, {
activePersonaIds: ["builtin:fizz"],
});
await page.goto("/");
await page.getByTestId("channel-bob-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
await waitForMockLiveSubscription(page, "bob-tyler");
const threadRoot = await emitMockMessage(
page,
"bob-tyler",
"Thread before adding an agent",
);
await emitMockMessage(page, "bob-tyler", "Existing thread reply", {
parentEventId: threadRoot.id,
});
const threadSummary = page.getByTestId("message-thread-summary").first();
await expect(threadSummary).toBeVisible();
await threadSummary.click();
const threadPanel = page.getByTestId("message-thread-panel");
const input = threadPanel.getByTestId("message-input");
await input.fill("Ask @fi");
await expect(
threadPanel
.getByTestId("mention-autocomplete")
.locator("button", { hasText: "Fizz" }),
).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" in this thread");
const baselineCommands = await readCommandLog(page);
await threadPanel.getByTestId("send-message").click();
await expect(
page.getByText(
"Agents must already be in a DM to be mentioned in its threads. Start a new conversation that includes the agent.",
),
).toBeVisible();
const commands = await readCommandLog(page);
expect(commandCount(commands, "create_managed_agent")).toBe(
commandCount(baselineCommands, "create_managed_agent"),
);
expect(commandCount(commands, "add_channel_members")).toBe(
commandCount(baselineCommands, "add_channel_members"),
);
await expect(input).toContainText("Fizz");
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
});
test("defers agent mentions until DM members finish loading", async ({
page,
}) => {
await installMockBridge(page, {
channelMembersReadDelayMs: 5_000,
managedAgents: [
{
pubkey: TEST_IDENTITIES.alice.pubkey,
name: "alice",
status: "stopped",
},
],
});
await page.goto("/");
await page.getByTestId("channel-alice-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
await waitForMockLiveSubscription(page, "alice-tyler");
const threadRoot = await emitMockMessage(
page,
"alice-tyler",
"Thread while members load",
);
await emitMockMessage(page, "alice-tyler", "Existing thread reply", {
parentEventId: threadRoot.id,
});
await page.getByTestId("message-thread-summary").first().click();
const threadPanel = page.getByTestId("message-thread-panel");
const input = threadPanel.getByTestId("message-input");
await input.fill("Ask @ali");
await expect(
threadPanel
.getByTestId("mention-autocomplete")
.locator("button", { hasText: "alice" }),
).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" before members resolve");
const baselineCommands = await readCommandLog(page);
await threadPanel.getByTestId("send-message").click();
await expect(
page.getByText(DM_THREAD_MEMBERS_LOADING_ERROR_TEXT).first(),
).toBeVisible();
await page.mouse.move(0, 0);
expect(commandCount(await readCommandLog(page), "add_channel_members")).toBe(
commandCount(baselineCommands, "add_channel_members"),
);
await expect(input).toContainText("before members resolve");
await page.waitForTimeout(5_100);
await threadPanel.getByTestId("send-message").click();
await expect(page.getByText(DM_THREAD_AGENT_MENTION_ERROR_TEXT)).toHaveCount(
0,
);
expect(commandCount(await readCommandLog(page), "add_channel_members")).toBe(
commandCount(baselineCommands, "add_channel_members"),
);
await expect(input).toHaveText("@alice ");
await expect(threadPanel).toContainText("before members resolve");
});
test("autocomplete filters managed-agent suggestions as user types", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.alice.pubkey,
name: "alice",
status: "stopped",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@ali");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("alice")).toBeVisible();
await expect(dropdown.getByText("bob")).not.toBeVisible();
});
test("autocomplete searches global non-member people from the first typed character", async ({
page,
}) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: CASEY_PROFILE_PUBKEY,
displayName: "tessa",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@t");
const dropdown = autocomplete(page);
const tessaRow = dropdown.locator("button", { hasText: "tessa" });
await expect(tessaRow).toBeVisible();
await expect(tessaRow.getByText("not in channel")).toBeVisible();
});
test("mention autocomplete caps global people search at 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 page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("message-input").fill("@Alex");
const dropdown = autocomplete(page);
await expect(dropdown.locator("button")).toHaveCount(50);
await expect(dropdown.getByText("Alex 50")).toBeVisible();
await expect(dropdown.getByText("Alex 55")).toHaveCount(0);
await expect(dropdown.getByText("not in channel").last()).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(searchCalls).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
payload: expect.objectContaining({ cursor: "2", limit: 50 }),
}),
]),
);
});
test("selecting a person mention inserts @Name into input", async ({
page,
}) => {
await page.addInitScript(() => {
window.localStorage.setItem("buzz-theme", "buzz-dark");
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
const dropdown = autocomplete(page);
for (let attempt = 0; attempt < 5; attempt++) {
await input.fill("Hey @bo");
await dropdown.getByText("bob").click();
await page.keyboard.type("hello");
await expect(input).toHaveText("Hey @bob hello");
}
const mentionChip = input.locator(".human-mention-highlight", {
hasText: "bob",
});
await expect(mentionChip).toBeVisible();
await expect(mentionChip).toHaveText("bob");
await expect(mentionChip).not.toHaveClass(/agent-mention-highlight/);
await expect(mentionChip).toHaveCSS("display", "inline");
await expect(
input.locator(".mention-prefix-hidden", { hasText: "@" }),
).toHaveCount(1);
const iconMask = await mentionChip.evaluate((element) =>
getComputedStyle(element, "::before").getPropertyValue(
"-webkit-mask-image",
),
);
expect(iconMask).toContain("data:image/svg+xml");
await expect(mentionChip).toHaveCSS("line-height", "18px");
const scrollViewport = page.getByTestId("message-input-scroll");
const paintedBounds = await mentionChip.evaluate((element) => {
const chip = element.getBoundingClientRect();
const viewport = element
.closest("[data-testid='message-input-scroll']")
?.getBoundingClientRect();
if (!viewport)
throw new Error("Mention chip is missing its scroll viewport");
return {
chipTop: chip.top,
chipBottom: chip.bottom,
viewportTop: viewport.top,
viewportBottom: viewport.bottom,
};
});
expect(paintedBounds.chipTop).toBeGreaterThanOrEqual(
paintedBounds.viewportTop,
);
expect(paintedBounds.chipBottom).toBeLessThanOrEqual(
paintedBounds.viewportBottom,
);
const humanIconTranslateY = await mentionChip.evaluate(
(element) =>
new DOMMatrix(getComputedStyle(element, "::before").transform).m42,
);
const channelIconTranslateY = await input.evaluate((composer) => {
const probe = document.createElement("span");
probe.className =
"mention-chip inline-chip-with-icon inline-chip-icon-channel";
composer.append(probe);
const translateY = new DOMMatrix(
getComputedStyle(probe, "::before").transform,
).m42;
probe.remove();
return translateY;
});
expect(humanIconTranslateY - channelIconTranslateY).toBeCloseTo(1);
await input.fill("@bo");
await dropdown.getByText("bob").click();
await input.press("Shift+Enter");
await page.keyboard.type("@bo");
await dropdown.getByText("bob").click();
await input.press("Shift+Enter");
await page.keyboard.type("@bo");
await dropdown.getByText("bob").click();
const multilineChips = input.locator(".human-mention-highlight");
await expect(multilineChips).toHaveCount(3);
const multilinePaintBounds = await multilineChips.evaluateAll((elements) => {
const viewport = elements[0]
?.closest("[data-testid='message-input-scroll']")
?.getBoundingClientRect();
if (!viewport) {
throw new Error("Mention chips are missing their scroll viewport");
}
return elements.map((element) => {
const chip = element.getBoundingClientRect();
return {
chipTop: chip.top,
chipBottom: chip.bottom,
viewportTop: viewport.top,
viewportBottom: viewport.bottom,
};
});
});
for (const bounds of multilinePaintBounds) {
expect(bounds.chipTop).toBeGreaterThanOrEqual(bounds.viewportTop);
expect(bounds.chipBottom).toBeLessThanOrEqual(bounds.viewportBottom);
}
await scrollViewport.evaluate((element) => {
element.style.width = "8rem";
});
await input.fill("A deliberately long prefix that forces @bo");
await dropdown.getByText("bob").click();
const wrappedChip = input.locator(".human-mention-highlight", {
hasText: "bob",
});
const wrappedPaintBounds = await wrappedChip.evaluate((element) => {
const chip = element.getBoundingClientRect();
const viewport = element
.closest("[data-testid='message-input-scroll']")
?.getBoundingClientRect();
if (!viewport)
throw new Error("Mention chip is missing its scroll viewport");
return {
chipTop: chip.top,
chipBottom: chip.bottom,
viewportTop: viewport.top,
viewportBottom: viewport.bottom,
};
});
expect(wrappedPaintBounds.chipTop).toBeGreaterThanOrEqual(
wrappedPaintBounds.viewportTop,
);
expect(wrappedPaintBounds.chipBottom).toBeLessThanOrEqual(
wrappedPaintBounds.viewportBottom,
);
await expect(input).toHaveCSS("height", /^(?!20px$)/);
await scrollViewport.evaluate((element) => {
element.style.removeProperty("width");
});
await waitForAnimations(page);
await page.getByTestId("message-composer").screenshot({
path: "test-results/inline-chip-polish/composer-after.png",
});
});
test("immediate ArrowLeft after a person mention is not bounced past the trailing space", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Hey @bo");
await autocomplete(page).getByText("bob").click();
await page.keyboard.press("ArrowLeft");
await page.keyboard.type("x");
const text = (await input.innerText()).replace(/\s+$/, "");
expect(text).toBe("Hey @bobx");
expect(text).not.toMatch(/@bob x/);
});
test("clicking a person mention chip edge is not treated as after the trailing space", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Hey @bo");
await autocomplete(page).getByText("bob").click();
const chip = input.locator(".human-mention-highlight", { hasText: "bob" });
await expect(chip).toBeVisible();
const box = await chip.boundingBox();
expect(box).toBeTruthy();
await chip.click({
position: {
x: Math.max((box?.width ?? 1) - 2, 0),
y: (box?.height ?? 2) / 2,
},
});
await page.keyboard.type("x");
const text = (await input.innerText()).replace(/\s+$/, "");
expect(text).toBe("Hey @bobx");
expect(text).not.toMatch(/@bob x/);
});
test("typing a mention before existing text does not interleave spaces", async ({
page,
}) => {
// Regression (the reported repro): with a draft already written, place the
// caret earlier in the message, type a partial mention, pick a suggestion,
// then keep typing. Caret correction used to fire on every document change
// and walk the caret across the mention's trailing space, so each keystroke
// pushed a space further into the rest of the draft.
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("hello world");
// Click between "hello" and " world", then open autocomplete there.
await input.focus();
for (let i = 0; i < " world".length; i++) {
await page.keyboard.press("ArrowLeft");
}
await page.keyboard.type(" @bo");
await autocomplete(page).getByText("bob").click();
await page.keyboard.type("abc");
await expect(input).toHaveText("hello @bob abc world");
});
test("typing an unregistered @token before existing text is left alone", async ({
page,
}) => {
// The trailing-space scan is purely textual, so it also fired for tokens
// that were never registered as mentions.
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("hello world");
await input.focus();
for (let i = 0; i < " world".length; i++) {
await page.keyboard.press("ArrowLeft");
}
await page.keyboard.type(" @zzq");
await expect(input).toHaveText("hello @zzq world");
});
test("channel references keep caret movement through the channel name", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("#general");
const channelChip = input.locator(".inline-chip-icon-channel", {
hasText: "general",
});
await expect(channelChip).toBeVisible();
await expect(channelChip).toHaveText("general");
await expect(
input.locator(".mention-prefix-hidden", { hasText: "#" }),
).toHaveCount(1);
const iconMask = await channelChip.evaluate((element) =>
getComputedStyle(element, "::before").getPropertyValue(
"-webkit-mask-image",
),
);
expect(iconMask).toContain("data:image/svg+xml");
await input.focus();
await input.press("ArrowLeft");
await input.press("ArrowLeft");
await input.press("ArrowLeft");
await page.keyboard.type("X");
await expect(input).toHaveText("#geneXral");
});
test("selecting a managed agent mention inserts @Name into input", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.alice.pubkey,
name: "alice",
status: "stopped",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
const dropdown = autocomplete(page);
for (let attempt = 0; attempt < 5; attempt++) {
await input.fill("Hey @ali");
await dropdown.getByText("alice").click();
await page.keyboard.type("hello");
await expect(input).toHaveText("Hey @alice hello");
}
const agentMentionChip = input.locator(".agent-mention-highlight", {
hasText: "alice",
});
await expect(agentMentionChip).toBeVisible();
await expect(agentMentionChip).toHaveText("alice");
await expect(agentMentionChip).toHaveCSS("display", "inline");
await expect(agentMentionChip).toHaveCSS("border-top-width", "0px");
});
test("selecting a persona mention creates a channel agent before sending", async ({
page,
}) => {
await installMockBridge(page, {
activePersonaIds: ["builtin:fizz"],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Ask @fi");
const dropdown = autocomplete(page);
const fizzRow = dropdown.locator("button", { hasText: "Fizz" });
await expect(fizzRow).toBeVisible();
await expect(fizzRow.getByTestId("mention-agent-icon")).toBeVisible();
await expect(fizzRow.getByText("agent")).toBeVisible();
await expect(fizzRow.getByText("not in channel")).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" for a hand");
const composerChip = input.locator(".agent-mention-highlight", {
hasText: "Fizz",
});
await expect(composerChip).toBeVisible();
await expect(composerChip).toHaveText("Fizz");
const baselineCommands = await readCommandLog(page);
const baselineCreateCount = commandCount(
baselineCommands,
"create_managed_agent",
);
const baselineAddCount = commandCount(
baselineCommands,
"add_channel_members",
);
const baselineStartCount = commandCount(
baselineCommands,
"start_managed_agent",
);
await page.getByTestId("send-message").click();
await expect(page.getByRole("alertdialog")).toHaveCount(0);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "create_managed_agent"),
)
.toBeGreaterThan(baselineCreateCount);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "add_channel_members"),
)
.toBeGreaterThan(baselineAddCount);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "start_managed_agent"),
)
.toBeGreaterThan(baselineStartCount);
await expect
.poll(async () => commandCount(await readCommandLog(page), "sign_event"))
.toBeGreaterThan(commandCount(baselineCommands, "sign_event"));
const commandsAfterSend = (await readCommandLog(page)).slice(
baselineCommands.length,
);
const startIndex = commandsAfterSend.indexOf("start_managed_agent");
const sendIndex = commandsAfterSend.indexOf("sign_event");
expect(startIndex).toBeGreaterThanOrEqual(0);
expect(sendIndex).toBeGreaterThanOrEqual(0);
expect(startIndex).toBeLessThan(sendIndex);
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention].agent-mention-highlight", { hasText: "Fizz" });
await expect(mentionChip).toBeVisible();
await expect(mentionChip).toHaveText("Fizz");
});
test("selecting a persona mention reuses an existing persona agent", async ({
page,
}) => {
await installMockBridge(page, {
activePersonaIds: ["builtin:fizz"],
managedAgents: [
{
pubkey: REUSABLE_PERSONA_AGENT_PUBKEY,
name: "Fizz",
personaId: "builtin:fizz",
status: "stopped",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Ask @fi");
const dropdown = autocomplete(page);
const fizzRow = dropdown.locator("button", { hasText: "Fizz" });
await expect(fizzRow).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" for a hand");
const baselineCommands = await readCommandLog(page);
const baselineCreateCount = commandCount(
baselineCommands,
"create_managed_agent",
);
const baselineAddCount = commandCount(
baselineCommands,
"add_channel_members",
);
const baselineStartCount = commandCount(
baselineCommands,
"start_managed_agent",
);
await page.getByTestId("send-message").click();
await expect(page.getByRole("alertdialog")).toHaveCount(0);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "add_channel_members"),
)
.toBeGreaterThan(baselineAddCount);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "start_managed_agent"),
)
.toBeGreaterThan(baselineStartCount);
expect(
commandCount(await readCommandLog(page), "create_managed_agent"),
).toEqual(baselineCreateCount);
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention].agent-mention-highlight", { hasText: "Fizz" });
await expect(mentionChip).toBeVisible();
await expect(mentionChip).toHaveText("Fizz");
});
test("managed relay-profile agents with member roles can be addressed explicitly", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/");
await openChannelBrowser(page);
await expect(page.getByTestId("channel-browser-dialog")).toBeVisible();
await page
.getByTestId("browse-channel-sales")
.getByRole("button", { name: "Join" })
.click();
await expect(page.getByTestId("chat-title")).toHaveText("sales");
const input = page.getByTestId("message-input");
await input.fill("@char");
const dropdown = autocomplete(page);
const charlieRow = dropdown.getByTestId(
`mention-suggestion-${TEST_IDENTITIES.charlie.pubkey}`,
);
await expect(charlieRow.getByText("charlie")).toBeVisible();
await expect(charlieRow.getByText("agent")).toBeVisible();
await charlieRow
.getByRole("button", {
name: "Automatically mention charlie",
exact: true,
})
.click();
await expect(input).toHaveText("@charlie ");
await expect(input.locator(".agent-mention-highlight")).toHaveText("charlie");
await expect(
page.getByTestId(`composer-address-lock-${TEST_IDENTITIES.charlie.pubkey}`),
).toBeVisible();
await expect(
page.getByRole("status").filter({
hasText: "Automatically mentioning charlie",
}),
).toBeVisible();
});
test("other-owned agents without a shared channel are hidden from mentions", async ({
page,
}) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: PROFILE_ONLY_AGENT_PUBKEY,
displayName: "mira",
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
isAgent: true,
},
],
userSearchDelayMs: 1_000,
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@mira");
const dropdown = autocomplete(page);
await expect(dropdown).not.toBeVisible();
await expect(input.locator(".mention-chip")).toHaveCount(0);
});
test("stale channel-member agents absent from managed and relay directories stay hidden", async ({
page,
}) => {
await installMockBridge(page, { userSearchDelayMs: 1_000 });
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@mira");
await expect(autocomplete(page)).toHaveCount(0);
});
test("managed relay agents are visible in channel mentions regardless of relay policy", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
status: "stopped",
},
],
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: ["deadbeef".repeat(8)],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@quinn");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("quinn")).toBeVisible();
await expect(dropdown.getByText("agent")).toBeVisible();
});
test("relay-only shared agents stay hidden from DM mentions", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-alice-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
await page.getByTestId("message-input").fill("@alice");
await expect(autocomplete(page)).toHaveCount(0);
});
test("cached relay-agent suggestions are removed when channel authorization disappears", async ({
page,
}) => {
await installMockBridge(page, { userSearchDelayMs: 10_000 });
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@alice");
const aliceSuggestion = autocomplete(page).getByTestId(
`mention-suggestion-${TEST_IDENTITIES.alice.pubkey}`,
);
await expect(aliceSuggestion).toBeVisible();
await expect
.poll(async () =>
(await readCommandPayloadLog(page)).some(
(entry) =>
entry.command === "search_users" &&
(entry.payload as { query?: string }).query === "alice",
),
)
.toBe(true);
await page.evaluate(async (channelId) => {
const bridge = window as Window & {
__BUZZ_E2E_INVALIDATE_CHANNELS__?: () => Promise<void>;
__BUZZ_E2E_MUTATE_CHANNEL__?: (opts: {
channelId: string;
channelType: null;
}) => void;
};
bridge.__BUZZ_E2E_MUTATE_CHANNEL__?.({ channelId, channelType: null });
await bridge.__BUZZ_E2E_INVALIDATE_CHANNELS__?.();
}, GENERAL_CHANNEL_ID);
await expect(aliceSuggestion).toHaveCount(0);
});
test("relay-only shared agents appear in forum mentions", async ({ page }) => {
await installMockBridge(page, {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["watercooler"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-watercooler").click();
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
await page.getByRole("button", { name: "Start a new post..." }).click();
await page.getByTestId("message-input").fill("@quinn");
await expect(
page.getByTestId("mention-autocomplete").getByText("quinn"),
).toBeVisible();
});
test("forum sends revalidate relay-agent authorization before signing", async ({
page,
}) => {
await installMockBridge(page, {
deferredComposerUploads: true,
uploadDescriptors: [
{
url: `https://mock.relay/media/${"f".repeat(64)}.pdf`,
sha256: "f".repeat(64),
size: 12345,
type: "application/pdf",
uploaded: Math.floor(Date.now() / 1000),
filename: "forum-race.pdf",
},
],
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["watercooler"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-watercooler").click();
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
await page.getByRole("button", { name: "Start a new post..." }).click();
const input = page.getByTestId("message-input");
await input.fill("@quinn");
await page.getByTestId("mention-autocomplete").getByText("quinn").click();
await page.keyboard.type("hello");
await page.getByRole("button", { name: "Attach file" }).click();
const removeAttachment = page.getByRole("button", {
name: "Remove attachment",
});
await expect(removeAttachment).toBeVisible();
await page.evaluate(() => {
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.agentListDelayMs = 1_000;
window.__BUZZ_E2E__.mock.relayAgentListErrors = Array(100).fill(
"mock forum directory revoked before send",
);
});
await page.getByTestId("send-message").click();
await expect(input).toHaveAttribute("contenteditable", "false");
await expect(removeAttachment).toBeDisabled();
await removeAttachment.evaluate((button: HTMLButtonElement) =>
button.click(),
);
await expect(removeAttachment).toBeVisible();
await input.focus();
await page.keyboard.type(" later edit");
await expect(input).toContainText("@quinn hello");
await expect(input).not.toContainText("later edit");
const outgoingContent = `@quinn hello\n[forum-race.pdf](https://mock.relay/media/${"f".repeat(64)}.pdf)`;
await expect
.poll(() => readOutgoingMentionPubkeys(page, outgoingContent))
.not.toBeNull();
await expect
.poll(() => readOutgoingMentionPubkeys(page, outgoingContent))
.not.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
});
test("managed agents use the channel roster for membership labels", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: IN_CHANNEL_MANAGED_AGENT_PUBKEY,
name: "carl",
status: "running",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect
.poll(() =>
page.evaluate(
(channelId) =>
window.__BUZZ_E2E_QUERY_CLIENT__?.getQueryState([
"channels",
channelId,
"members",
])?.status,
GENERAL_CHANNEL_ID,
),
)
.toBe("success");
await page.evaluate(
async ({ channelId, pubkey }) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock bridge is not installed.");
await invoke("add_channel_members", {
channelId,
pubkeys: [pubkey],
role: "bot",
});
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
exact: true,
});
},
{
channelId: GENERAL_CHANNEL_ID,
pubkey: IN_CHANNEL_MANAGED_AGENT_PUBKEY,
},
);
const input = page.getByTestId("message-input");
await input.fill("@carl");
const carlRow = autocomplete(page).locator("button", { hasText: "carl" });
await expect(carlRow).toBeVisible();
await expect(carlRow.getByText("agent")).toBeVisible();
await expect(carlRow.getByText("not in channel")).toHaveCount(0);
});
test("relay-agent directory errors fail closed and recover after a fresh fetch", async ({
page,
}) => {
await installMockBridge(page, {
relayAgentListErrors: ["mock directory unavailable", null],
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
const input = page.getByTestId("message-input");
await input.fill("@quinn");
await expect(autocomplete(page)).toHaveCount(0);
await page.evaluate(async () => {
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["relay-agents"],
});
});
await expect(autocomplete(page).getByText("quinn")).toBeVisible();
await page.evaluate(() => {
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.agentListDelayMs = 1_000;
void window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["relay-agents"],
});
});
await expect
.poll(async () =>
page.evaluate(
() =>
window.__BUZZ_E2E_QUERY_CLIENT__?.getQueryState(["relay-agents"])
?.fetchStatus,
),
)
.toBe("fetching");
await expect(autocomplete(page).getByText("quinn")).toBeVisible({
timeout: 200,
});
await expect
.poll(async () =>
page.evaluate(
() =>
window.__BUZZ_E2E_QUERY_CLIENT__?.getQueryState(["relay-agents"])
?.fetchStatus,
),
)
.toBe("idle");
await expect(autocomplete(page).getByText("quinn")).toBeVisible();
});
test("relay-only allowlisted agents emit a p tag when sent", async ({
page,
}) => {
await installMockBridge(page, {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.evaluate(
async ({ channelId, pubkey }) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock bridge is not installed.");
await invoke("add_channel_members", {
channelId,
pubkeys: [pubkey],
role: "bot",
});
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
});
},
{
channelId: GENERAL_CHANNEL_ID,
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
},
);
const input = page.getByTestId("message-input");
for (let attempt = 0; attempt < 5; attempt++) {
await input.fill("@quinn");
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
await expect(quinnRow).toBeVisible();
await quinnRow.click();
await page.keyboard.type("hello");
await expect(input).toHaveText("@quinn hello");
}
const baselineCommands = await readCommandLog(page);
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
const commands = await readCommandLog(page);
expect(commandCount(commands, "revalidate_relay_agents")).toBe(
commandCount(baselineCommands, "revalidate_relay_agents") + 2,
);
expect(commandCount(commands, "list_relay_agents")).toBe(
commandCount(baselineCommands, "list_relay_agents"),
);
});
test("managed agents keep their p tag when relay discovery fails before send", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
status: "running",
},
],
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
const input = page.getByTestId("message-input");
await input.fill("@quinn");
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
await expect(quinnRow).toBeVisible();
await quinnRow.click();
await expect(input).toHaveText("@quinn ");
await page.keyboard.type("hello");
await expect(input).toHaveText("@quinn hello");
await page.evaluate(() => {
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.relayAgentListErrors = Array(5).fill(
"mock unrelated relay directory failure",
);
});
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
});
test("targeted revocation before send causes no agent side effects", async ({
page,
}) => {
await installMockBridge(page, {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.evaluate(
async ({ channelId, pubkey }) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock bridge is not installed.");
await invoke("add_channel_members", {
channelId,
pubkeys: [pubkey],
role: "bot",
});
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
});
},
{
channelId: GENERAL_CHANNEL_ID,
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
},
);
const input = page.getByTestId("message-input");
await input.fill("@quinn");
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
await expect(quinnRow).toBeVisible();
await quinnRow.click();
await page.keyboard.type("hello");
await page.evaluate((pubkey) => {
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.relayAgentRevalidationRevokedPubkeys = [pubkey];
}, ALLOWLIST_RELAY_AGENT_PUBKEY);
const baselineCommands = await readCommandLog(page);
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.not.toBeNull();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.not.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
const commands = await readCommandLog(page);
expect(commandCount(commands, "revalidate_relay_agents")).toBe(
commandCount(baselineCommands, "revalidate_relay_agents") + 2,
);
expect(commandCount(commands, "list_relay_agents")).toBe(
commandCount(baselineCommands, "list_relay_agents"),
);
for (const command of [
"add_channel_members",
"start_managed_agent",
"attach_managed_agent",
"sync_agents_to_active_huddle",
]) {
expect(commandCount(commands, command)).toBe(
commandCount(baselineCommands, command),
);
}
});
test("selected relay agents are invited as bots before sending", async ({
page,
}) => {
await installMockBridge(page, {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
const input = page.getByTestId("message-input");
await input.fill("@quinn");
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
await expect(quinnRow).toBeVisible();
await expect(quinnRow.getByText("not in channel")).toHaveCount(0);
await quinnRow.click();
await page.keyboard.type("hello");
const baselinePayloadCount = (await readCommandPayloadLog(page)).length;
await page.getByTestId("send-message").click();
const inviteButton = page.getByRole("button", {
name: "Invite",
exact: true,
});
await expect(inviteButton).toBeVisible();
await inviteButton.click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
const sendCommands = (await readCommandPayloadLog(page)).slice(
baselinePayloadCount,
);
const addCommand = sendCommands.find(
(entry) => entry.command === "add_channel_members",
);
expect(addCommand?.payload).toMatchObject({
channelId: GENERAL_CHANNEL_ID,
pubkeys: [ALLOWLIST_RELAY_AGENT_PUBKEY],
role: "bot",
});
});
test("selected relay agents revoked after the invite prompt cause no side effects", async ({
page,
}) => {
await installMockBridge(page, {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
const input = page.getByTestId("message-input");
await input.fill("@quinn");
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
await expect(quinnRow).toBeVisible();
await quinnRow.click();
await page.keyboard.type("hello");
await page.getByTestId("send-message").click();
const inviteButton = page.getByRole("button", {
name: "Invite",
exact: true,
});
await expect(inviteButton).toBeVisible();
await page.evaluate(() => {
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.relayAgentListErrors = Array(5).fill(
"mock directory revoked after invite prompt",
);
});
const baselineCommands = await readCommandLog(page);
await inviteButton.click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.not.toBeNull();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.not.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
const commands = await readCommandLog(page);
for (const command of [
"add_channel_members",
"start_managed_agent",
"attach_managed_agent",
"sync_agents_to_active_huddle",
]) {
expect(commandCount(commands, command)).toBe(
commandCount(baselineCommands, command),
);
}
});
test("selected relay agents revoked during send emit no p tag", async ({
page,
}) => {
await installMockBridge(page, {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
const input = page.getByTestId("message-input");
await input.fill("@quinn");
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
await expect(quinnRow).toBeVisible();
await quinnRow.click();
await page.keyboard.type("hello");
await page.evaluate(() => {
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.agentListDelayMs = 300;
});
await page.getByTestId("send-message").click();
await page.getByRole("button", { name: "Invite", exact: true }).click();
await page.evaluate(() => {
window.__BUZZ_E2E__.mock ??= {};
window.__BUZZ_E2E__.mock.relayAgentListErrors = Array(100).fill(
"mock directory revoked mid-send",
);
});
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.not.toBeNull();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.not.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
});
test("owner-only builds admit cross-owner relay agents authorized by allowlist", async ({
page,
}) => {
await installMockBridge(page, {
ownerOnlyAccessBuild: true,
searchProfiles: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
displayName: "quinn",
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
isAgent: true,
},
],
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.evaluate(
async ({ channelId, pubkey }) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock bridge is not installed.");
await invoke("add_channel_members", {
channelId,
pubkeys: [pubkey],
role: "bot",
});
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
});
},
{
channelId: GENERAL_CHANNEL_ID,
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
},
);
const input = page.getByTestId("message-input");
await input.fill("@quinn");
const quinnRow = autocomplete(page).locator("button", { hasText: "quinn" });
await expect(quinnRow).toBeVisible();
await quinnRow.click();
await page.keyboard.type("hello");
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@quinn hello"))
.toContain(ALLOWLIST_RELAY_AGENT_PUBKEY);
});
test("owner-only builds show verified same-owner relay agents", async ({
page,
}) => {
await installMockBridge(page, {
ownerOnlyAccessBuild: true,
searchProfiles: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
displayName: "quinn",
ownerPubkey: MOCK_VIEWER_PUBKEY,
isAgent: true,
},
],
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
ownerPubkey: MOCK_VIEWER_PUBKEY,
name: "quinn",
respondTo: "owner-only",
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.getByTestId("message-input").fill("@quinn");
await expect(autocomplete(page).getByText("quinn")).toBeVisible();
});
test("relay-only allowlisted agents stay hidden outside their channel", async ({
page,
}) => {
await installMockBridge(page, {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["agents"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("message-input").fill("@quinn");
await expect(autocomplete(page)).toHaveCount(0);
});
test("owner-only builds admit cross-owner relay agents authorized for anyone", async ({
page,
}) => {
await installMockBridge(page, {
ownerOnlyAccessBuild: true,
searchProfiles: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
displayName: "quinn",
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
isAgent: true,
},
],
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
ownerPubkey: TEST_IDENTITIES.outsider.pubkey,
name: "quinn",
respondTo: "anyone",
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("message-input").fill("@quinn");
await expect(autocomplete(page).getByText("quinn")).toBeVisible();
});
test("relay-only excluded agents stay hidden from channel mentions", async ({
page,
}) => {
await installMockBridge(page, {
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [TEST_IDENTITIES.outsider.pubkey],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("message-input").fill("@quinn");
await expect(autocomplete(page)).toHaveCount(0);
});
test("shared agents wait for initial directory authorization", async ({
page,
}) => {
await installMockBridge(page, {
agentListDelayMs: 1_000,
relayAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.getByTestId("message-input").fill("@quinn");
await expect(autocomplete(page)).toHaveCount(0);
await expect(autocomplete(page).getByText("quinn")).toBeVisible({
timeout: 3_000,
});
});
test("mentioning an in-channel stopped managed agent starts it before sending", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: IN_CHANNEL_MANAGED_AGENT_PUBKEY,
name: "fizz",
status: "stopped",
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Hey @fizz");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("fizz")).toBeVisible();
await expect(dropdown.getByText("agent")).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" can you help?");
const baselineStartCount = commandCount(
await readCommandLog(page),
"start_managed_agent",
);
await page.getByTestId("send-message").click();
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "start_managed_agent"),
)
.toBeGreaterThan(baselineStartCount);
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention].agent-mention-highlight", { hasText: "fizz" });
await expect(mentionChip).toBeVisible();
});
test("mentioning an in-channel provider managed agent deploys it before sending", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: OUT_OF_CHANNEL_PROVIDER_AGENT_PUBKEY,
name: "portal",
status: "not_deployed",
channelNames: ["general"],
backend: {
type: "provider",
id: "portal",
config: { region: "test" },
},
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Hey @portal");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("portal")).toBeVisible();
await expect(dropdown.getByText("agent")).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" can you help?");
const baselineStartCount = commandCount(
await readCommandLog(page),
"start_managed_agent",
);
await page.getByTestId("send-message").click();
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "start_managed_agent"),
)
.toBeGreaterThan(baselineStartCount);
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention].agent-mention-highlight", { hasText: "portal" });
await expect(mentionChip).toBeVisible();
});
test("mentioning a non-member managed agent adds and starts it before sending", async ({
page,
}) => {
await installMockBridge(page, {
personas: [
{
id: "persona-owner",
displayName: "Fizz",
systemPrompt: "",
},
],
managedAgents: [
{
pubkey: OUT_OF_CHANNEL_MANAGED_AGENT_PUBKEY,
name: "fizz",
personaId: "persona-owner",
status: "stopped",
respondTo: "anyone",
respondToAllowlist: [TEST_IDENTITIES.outsider.pubkey],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Loop in @fizz");
const dropdown = autocomplete(page);
const fizzRow = dropdown.locator("button", { hasText: "fizz" });
await expect(fizzRow).toBeVisible();
await expect(fizzRow.getByText("not in channel")).toBeVisible();
await input.press("Enter");
const baselineCommands = await readCommandLog(page);
const baselineAddCount = commandCount(
baselineCommands,
"add_channel_members",
);
const baselineStartCount = commandCount(
baselineCommands,
"start_managed_agent",
);
const baselinePayloadCount = (await readCommandPayloadLog(page)).length;
await page.getByTestId("send-message").click();
await expect(page.getByRole("alertdialog")).toHaveCount(0);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "add_channel_members"),
)
.toBeGreaterThan(baselineAddCount);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "start_managed_agent"),
)
.toBeGreaterThan(baselineStartCount);
const sendCommands = (await readCommandPayloadLog(page)).slice(
baselinePayloadCount,
);
const updateIndex = sendCommands.findIndex(
(entry) => entry.command === "update_managed_agent",
);
const addIndex = sendCommands.findIndex(
(entry) => entry.command === "add_channel_members",
);
const startIndex = sendCommands.findIndex(
(entry) => entry.command === "start_managed_agent",
);
expect(updateIndex).toBeGreaterThanOrEqual(0);
expect(updateIndex).toBeLessThan(addIndex);
expect(updateIndex).toBeLessThan(startIndex);
expect(sendCommands[updateIndex]?.payload).toMatchObject({
input: {
pubkey: OUT_OF_CHANNEL_MANAGED_AGENT_PUBKEY,
respondTo: "owner-only",
respondToAllowlist: [],
},
});
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention].agent-mention-highlight", { hasText: "fizz" });
await expect(mentionChip).toBeVisible();
const persistedPolicy = await page.evaluate(async (pubkey) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock bridge is not installed.");
const agents = (await invoke("list_managed_agents", {})) as Array<{
pubkey: string;
respond_to: string;
respond_to_allowlist: string[];
}>;
const agent = agents.find((candidate) => candidate.pubkey === pubkey);
return agent
? {
respondTo: agent.respond_to,
respondToAllowlist: agent.respond_to_allowlist,
}
: null;
}, OUT_OF_CHANNEL_MANAGED_AGENT_PUBKEY);
expect(persistedPolicy).toEqual({
respondTo: "owner-only",
respondToAllowlist: [],
});
});
test("mentioning a non-member provider managed agent deploys it before sending", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: OUT_OF_CHANNEL_PROVIDER_AGENT_PUBKEY,
name: "portal",
status: "not_deployed",
backend: {
type: "provider",
id: "portal",
config: { region: "test" },
},
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Loop in @portal");
const dropdown = autocomplete(page);
const portalRow = dropdown.locator("button", { hasText: "portal" });
await expect(portalRow).toBeVisible();
await expect(portalRow.getByText("not in channel")).toBeVisible();
await input.press("Enter");
const baselineCommands = await readCommandLog(page);
const baselineAddCount = commandCount(
baselineCommands,
"add_channel_members",
);
const baselineStartCount = commandCount(
baselineCommands,
"start_managed_agent",
);
await page.getByTestId("send-message").click();
await expect(page.getByRole("alertdialog")).toHaveCount(0);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "add_channel_members"),
)
.toBeGreaterThan(baselineAddCount);
await expect
.poll(async () =>
commandCount(await readCommandLog(page), "start_managed_agent"),
)
.toBeGreaterThan(baselineStartCount);
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention].agent-mention-highlight", { hasText: "portal" });
await expect(mentionChip).toBeVisible();
});
test("system add rows use plain names while remove rows retain agent mention styling", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: OUT_OF_CHANNEL_PROVIDER_AGENT_PUBKEY,
name: "portal",
status: "deployed",
backend: {
type: "provider",
id: "portal",
config: { region: "test" },
},
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general", SYSTEM_MESSAGE_KIND);
await page.evaluate(
({ actorPubkey, kind, targetPubkey }) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: JSON.stringify({
type: "member_joined",
actor: actorPubkey,
target: targetPubkey,
}),
kind,
});
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: JSON.stringify({
type: "member_removed",
actor: actorPubkey,
target: targetPubkey,
}),
kind,
});
},
{
actorPubkey: TEST_IDENTITIES.tyler.pubkey,
kind: SYSTEM_MESSAGE_KIND,
targetPubkey: OUT_OF_CHANNEL_PROVIDER_AGENT_PUBKEY,
},
);
const addedRow = page
.getByTestId("system-message-row")
.filter({ hasText: "portal" })
.filter({ hasText: "added by" });
const removedRow = page
.getByTestId("system-message-row")
.filter({ hasText: "removed portal from the channel" });
const addedName = addedRow.getByText("portal", { exact: true });
await expect(addedName).toBeVisible();
await expect(addedName).not.toHaveAttribute("data-mention");
await expect(
removedRow.locator("[data-mention].agent-mention-highlight", {
hasText: "portal",
}),
).toHaveText("portal");
});
test("groups contiguous arrival activity with hidden names in the standard tooltip", async ({
page,
}) => {
const actor = {
pubkey: "10".repeat(32),
displayName: "Alice Chen",
};
const targets = [
{ pubkey: "11".repeat(32), displayName: "Erica Chapman" },
{ pubkey: "12".repeat(32), displayName: "Peter Griffin" },
{ pubkey: "13".repeat(32), displayName: "Marcia Thomas" },
{ pubkey: "14".repeat(32), displayName: "Jordan Lee" },
{ pubkey: "15".repeat(32), displayName: "Olivia Park" },
{ pubkey: "16".repeat(32), displayName: "Sam Rivera" },
];
await installMockBridge(page, {
searchProfiles: [actor, ...targets],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general", SYSTEM_MESSAGE_KIND);
await page.evaluate(
({ actorPubkey, addedTargets, kind }) => {
const createdAt = Math.floor(Date.now() / 1_000);
for (const [index, target] of addedTargets.entries()) {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: JSON.stringify({
type: "member_joined",
actor: actorPubkey,
target: target.pubkey,
}),
createdAt: createdAt + index,
kind,
});
}
},
{
actorPubkey: actor.pubkey,
addedTargets: targets,
kind: SYSTEM_MESSAGE_KIND,
},
);
await waitForTimelineSettled(page);
const groupedRow = page
.getByTestId("system-message-row")
.filter({ hasText: "added by Alice Chen, along with" });
for (const visibleName of [
"Erica Chapman",
"Peter Griffin",
"Marcia Thomas",
]) {
await expect(groupedRow).toContainText(visibleName);
}
await expect(
groupedRow.locator("p").filter({ hasText: "added by Alice Chen" }),
).toContainText(
"Erica Chapman added by Alice Chen, along with Peter Griffin, Marcia Thomas, Jordan Lee, and 2 others",
);
const avatarStack = groupedRow.getByTestId("system-message-avatar-stack");
await expect(avatarStack).toHaveCount(1);
await expect(avatarStack.getByTestId("system-message-avatar")).toHaveCount(5);
await expect(
groupedRow.locator("p").filter({ hasText: "added by Alice Chen" }),
).toHaveCSS("text-align", "left");
await expect(groupedRow.locator("[data-mention]")).toHaveCount(0);
const visibleName = groupedRow.getByText("Peter Griffin", { exact: true });
await expect(visibleName).toHaveCSS("text-decoration-line", "none");
await visibleName.hover();
await expect(visibleName).toHaveCSS("text-decoration-line", "underline");
const othersTrigger = groupedRow.getByRole("button", { name: "2 others" });
// Park the pointer off-target first: the previous hover leaves the mouse at a
// fixed viewport point, and any later reflow (new rows, scroll-to-bottom, a
// different text wrap) can slide this button under it. Without this the
// assertion measures where the mouse happens to be, not the resting style.
await page.mouse.move(0, 0);
await expect(othersTrigger).toHaveCSS("text-decoration-line", "none");
await othersTrigger.hover();
await expect(othersTrigger).toHaveCSS("text-decoration-line", "underline");
const tooltip = page.getByRole("tooltip");
await expect(tooltip).toContainText("Olivia Park");
await expect(tooltip).toContainText("Sam Rivera");
await expect(avatarStack.locator("..")).toHaveCSS("align-items", "center");
});
test("system agent profile exposes owned agent actions", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general", SYSTEM_MESSAGE_KIND);
await page.evaluate(
({ actorPubkey, kind, targetPubkey }) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: JSON.stringify({
type: "member_joined",
actor: actorPubkey,
target: targetPubkey,
}),
kind,
});
},
{
actorPubkey: TEST_IDENTITIES.tyler.pubkey,
kind: SYSTEM_MESSAGE_KIND,
targetPubkey: PROFILE_ONLY_AGENT_PUBKEY,
},
);
await waitForTimelineSettled(page);
const joinedRow = page
.getByTestId("system-message-row")
.filter({ hasText: "mira" })
.filter({ hasText: "added by" });
const agentName = joinedRow.getByText("mira", { exact: true });
await expect(agentName).toHaveText("mira");
await expect(agentName).not.toHaveAttribute("data-mention");
await agentName.hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expectOwnedAgentProfileActions(
profilePopover,
PROFILE_ONLY_AGENT_PUBKEY,
);
});
test("system agent activity avatar stack is decorative", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await waitForMockLiveSubscription(page, "random", SYSTEM_MESSAGE_KIND);
await page.evaluate(
({ kind, targetPubkey }) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "random",
content: JSON.stringify({
type: "member_joined",
actor: targetPubkey,
target: targetPubkey,
}),
kind,
});
},
{
kind: SYSTEM_MESSAGE_KIND,
targetPubkey: PROFILE_ONLY_AGENT_PUBKEY,
},
);
await waitForTimelineSettled(page);
const joinedRow = page
.getByTestId("system-message-row")
.filter({ hasText: "mira" })
.filter({ hasText: "joined the channel" });
const avatarStack = joinedRow.getByTestId("system-message-avatar-stack");
await expect(avatarStack.getByTestId("system-message-avatar")).toHaveCount(1);
await expect(avatarStack.locator("button")).toHaveCount(0);
});
test("membership activity folds a member joining then leaving", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-random").click();
await expect(page.getByTestId("chat-title")).toHaveText("random");
await waitForMockLiveSubscription(page, "random", SYSTEM_MESSAGE_KIND);
await page.evaluate(
({ alicePubkey, kind }) => {
const createdAt = Math.floor(Date.now() / 1_000);
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "random",
content: JSON.stringify({
type: "member_joined",
actor: alicePubkey,
target: alicePubkey,
}),
createdAt,
kind,
});
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "random",
content: JSON.stringify({ type: "member_left", actor: alicePubkey }),
createdAt: createdAt + 1,
kind,
});
},
{ alicePubkey: TEST_IDENTITIES.alice.pubkey, kind: SYSTEM_MESSAGE_KIND },
);
await waitForTimelineSettled(page);
const lifecycleRow = page
.getByTestId("system-message-row")
.filter({ hasText: "alice" })
.filter({ hasText: "joined, then left the channel" });
await expect(lifecycleRow).toBeVisible();
await expect(lifecycleRow.getByTestId("system-message-avatar")).toHaveCount(
1,
);
});
test("profile-only agent author hides actions without agent access", async ({
page,
}) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: PROFILE_ONLY_AGENT_PUBKEY,
displayName: "mira",
isAgent: true,
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Mira status update.", {
pubkey: PROFILE_ONLY_AGENT_PUBKEY,
});
await waitForTimelineSettled(page);
const messageRow = page
.getByTestId("message-row")
.filter({ hasText: "Mira status update." })
.first();
await messageRow.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expectAgentProfileActionsHidden(
profilePopover,
PROFILE_ONLY_AGENT_PUBKEY,
);
});
test("system member-joined rows render the joined person as a plain profile name", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general", SYSTEM_MESSAGE_KIND);
await page.evaluate(
({ kind, pubkey }) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: JSON.stringify({
type: "member_joined",
actor: pubkey,
target: pubkey,
}),
kind,
});
},
{ kind: SYSTEM_MESSAGE_KIND, pubkey: TEST_IDENTITIES.bob.pubkey },
);
await waitForTimelineSettled(page);
const joinedRow = page
.getByTestId("system-message-row")
.filter({ hasText: "bob" })
.filter({ hasText: "joined the channel" });
const joinedPersonName = joinedRow.getByText("bob", { exact: true });
await expect(joinedPersonName).toBeVisible();
await expect(joinedPersonName).not.toHaveAttribute("data-mention");
});
test("a managed non-member agent from a DM can be addressed explicitly", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/");
await page.getByTestId("channel-bob-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
const input = page.getByTestId("message-input");
await input.fill("@char");
const dropdown = autocomplete(page);
const charlieRow = dropdown.getByTestId(
`mention-suggestion-${TEST_IDENTITIES.charlie.pubkey}`,
);
await expect(charlieRow.getByText("charlie")).toBeVisible();
await expect(autocomplete(page)).toHaveCount(1);
await expect(input.locator(".mention-chip")).toHaveCount(0);
await charlieRow
.getByRole("button", {
name: "Automatically mention charlie",
exact: true,
})
.click();
await expect(input).toHaveText("@charlie ");
await expect(input.locator(".agent-mention-highlight")).toHaveText("charlie");
await expect(
page.getByTestId(`composer-address-lock-${TEST_IDENTITIES.charlie.pubkey}`),
).toBeVisible();
});
test("global non-member people can be selected from channel mentions", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Loop in @out");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("outsider")).toBeVisible();
await expect(dropdown.getByText("not in channel")).toBeVisible();
});
test("duplicate global people with the same visible identity collapse in channel mentions", async ({
page,
}) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: CASEY_PROFILE_PUBKEY,
displayName: "Pip",
},
{
pubkey:
"2222222222222222222222222222222222222222222222222222222222222222",
displayName: "Pip",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@pip");
const dropdown = autocomplete(page);
await expect(dropdown.locator("button", { hasText: "Pip" })).toHaveCount(1);
});
test("sent non-member person mention uses the normal mention style", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-bob-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
const input = page.getByTestId("message-input");
await input.fill("Loop in @out");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("outsider")).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" please");
await page.getByTestId("send-message").click();
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention]", { hasText: "outsider" });
await expect(mentionChip).toBeVisible();
await expect(mentionChip).toHaveClass(/inline-chip-icon-human/);
});
test("sent managed non-member agent mention uses the agent mention style", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/");
await page.getByTestId("channel-bob-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
const input = page.getByTestId("message-input");
await input.fill("Loop in @char");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("charlie")).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" too");
await page.getByTestId("send-message").click();
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention]", { hasText: "charlie" });
await expect(mentionChip).toBeVisible();
await expect(mentionChip).toHaveText("charlie");
await expect(mentionChip).toHaveClass(/agent-mention-highlight/);
});
test("mention button opens autocomplete and inserts a selected member", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Hey ");
await page.getByTestId("message-insert-mention").click();
const dropdown = autocomplete(page);
await expect(dropdown).toBeVisible();
await dropdown.getByText("bob").click();
await expect(input).toHaveText("Hey @bob ");
});
test("inserting a mention preserves Shift+Enter newlines (regression: bug #2)", async ({
page,
}) => {
// Before PR #618, mention insertion round-tripped through
// `setContent(markdown)`, which collapsed every Shift+Enter hard
// break to a single space. After the fix, autocomplete uses a
// native ProseMirror `tr.insertText` transaction and the line
// breaks survive.
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.click();
await page.keyboard.type("line one");
await page.keyboard.press("Shift+Enter");
await page.keyboard.type("line two @bo");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("bob")).toBeVisible();
await dropdown.getByText("bob").click();
// Both lines must still be present, separated by a real line break
// (rendered as a `<br>` by Tiptap; the projection sees `\n`).
await expect(input).toHaveText(/line one[\s\S]*line two @bob/);
await expect(input.locator("br")).toHaveCount(1);
});
test("keyboard navigation selects mention with Enter", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@bo");
const dropdown = autocomplete(page);
await expect(dropdown.getByText("bob")).toBeVisible();
// Press Enter to select the first (and only) suggestion
await input.press("Enter");
// Should insert @bob and NOT send the message
await expect(input).toHaveText("@bob ");
});
test("Escape dismisses autocomplete dropdown", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("@");
const dropdown = autocomplete(page);
await expect(dropdown).toBeVisible();
await input.press("Escape");
await expect(dropdown).not.toBeVisible();
});
test("mention text is highlighted in sent messages", async ({ page }) => {
const suffix = ` check this out ${Date.now()}`;
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const input = page.getByTestId("message-input");
await input.fill("Hey @bo");
await autocomplete(page).getByText("bob").click();
await expect(input).toHaveText("Hey @bob ");
await page.keyboard.type(suffix);
await page.getByTestId("send-message").click();
await waitForTimelineSettled(page);
const mentionChip = page
.getByTestId("message-row")
.last()
.locator("[data-mention].mention-chip", { hasText: "bob" });
await expect(mentionChip).toBeVisible();
await expect(mentionChip).toHaveText("bob");
await expect(mentionChip).toHaveClass(/inline-chip-icon-human/);
});
test("clicking author name opens user profile panel", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
// The seed message in general is from the mock identity (npub1mock...)
const firstMessage = page.getByTestId("message-row").first();
const authorButton = firstMessage.locator("button", {
hasText: "npub1mock...",
});
await authorButton.click();
// Click now opens the full profile panel instead of the popover
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible();
await expect(panel).toContainText("deadbeef");
});
test("hovering avatar opens popover, clicking opens profile panel", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
const firstMessage = page.getByTestId("message-row").first();
const avatarButton = firstMessage.locator("button").first();
// Hover should open the popover
await avatarButton.hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
// Click should close the popover and open the profile panel
await avatarButton.click();
await expect(profilePopover).toHaveCount(0);
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
});
test("clicking a mention chip in the timeline opens the profile panel", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Ping @bob about the launch", {
mentionPubkeys: [TEST_IDENTITIES.bob.pubkey],
});
await waitForTimelineSettled(page);
const mentionChip = page
.getByTestId("message-row")
.filter({ hasText: "Ping bob about the launch" })
.locator("[data-mention]", { hasText: "bob" });
await expect(mentionChip).toBeVisible();
await mentionChip.click();
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible();
await expect(panel).toContainText("bob");
});
test("mention text matching the kind-0 name alias resolves and opens the profile panel", async ({
page,
}) => {
// bob's mock profile has display_name "bob" and kind-0 name "bobby". A
// message that says "@bobby" (how agents/CLI resolve mentions at send time)
// must still render a clickable chip bound to bob's pubkey.
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Ask @bobby to review the doc", {
mentionPubkeys: [TEST_IDENTITIES.bob.pubkey],
});
await waitForTimelineSettled(page);
const mentionChip = page
.getByTestId("message-row")
.filter({ hasText: "Ask bobby to review the doc" })
.locator("[data-mention]", { hasText: "bobby" });
await expect(mentionChip).toBeVisible();
await mentionChip.click();
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible();
await expect(panel).toContainText("bob");
});
test("clicking a mention chip in a forum post opens the profile panel", async ({
page,
}) => {
await page.goto("/");
// Seed the forum post before entering the channel — forum views load from
// the mock store on fetch, so no live subscription is needed.
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await emitMockMessage(page, "watercooler", "Welcome aboard @bob!", {
kind: 45001,
mentionPubkeys: [TEST_IDENTITIES.bob.pubkey],
});
await page.getByTestId("channel-watercooler").click();
await expect(page.getByTestId("chat-title")).toHaveText("watercooler");
const mentionChip = page.locator("[data-mention]", { hasText: "bob" });
await expect(mentionChip).toBeVisible();
await mentionChip.click();
const panel = page.getByTestId("user-profile-panel");
await expect(panel).toBeVisible();
await expect(panel).toContainText("bob");
// The chip click must not bubble into the card and open the thread view.
await expect(page.getByRole("button", { name: "Back to posts" })).toHaveCount(
0,
);
});
test("agent profile popover shows its owner", async ({ page }) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
displayName: "Pollen",
ownerPubkey: TEST_IDENTITIES.bob.pubkey,
isAgent: true,
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Pollen checking in.", {
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
});
await waitForTimelineSettled(page);
const pollenMessage = page
.getByTestId("message-row")
.filter({ hasText: "Pollen checking in." })
.first();
await pollenMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-owner-${OWNED_AGENT_PROFILE_PUBKEY}`,
),
).toHaveText("managed by bob");
});
test("agent profile popover labels an agent owned by the viewer as you", async ({
page,
}) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
displayName: "Pollen",
ownerPubkey: MOCK_VIEWER_PUBKEY,
isAgent: true,
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Pollen checking in.", {
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
});
await waitForTimelineSettled(page);
const pollenMessage = page
.getByTestId("message-row")
.filter({ hasText: "Pollen checking in." })
.first();
await pollenMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-owner-${OWNED_AGENT_PROFILE_PUBKEY}`,
),
).toHaveText("managed by you");
});
test("agent profile popover falls back to the owner's pubkey", async ({
page,
}) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
displayName: "Pollen",
ownerPubkey: CASEY_PROFILE_PUBKEY,
isAgent: true,
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Pollen checking in.", {
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
});
await waitForTimelineSettled(page);
const pollenMessage = page
.getByTestId("message-row")
.filter({ hasText: "Pollen checking in." })
.first();
await pollenMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-owner-${OWNED_AGENT_PROFILE_PUBKEY}`,
),
).toHaveText("managed by 11111111…1111");
});
test("human profile popover does not show an owner", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Bob checking in.", {
pubkey: TEST_IDENTITIES.bob.pubkey,
});
await waitForTimelineSettled(page);
const bobMessage = page
.getByTestId("message-row")
.filter({ hasText: "Bob checking in." })
.first();
await bobMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expect(
profilePopover.locator('[data-testid^="user-profile-popover-owner-"]'),
).toHaveCount(0);
});
test("owned bot profile exposes message and huddle actions", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "online",
},
],
});
await page.goto("/");
await page.getByTestId("channel-agents").click();
await expect(page.getByTestId("chat-title")).toHaveText("agents");
const charlieMessage = page
.getByTestId("message-row")
.filter({ hasText: "Indexing the channel catalog now." })
.first();
await charlieMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expectOwnedAgentProfileActions(
profilePopover,
TEST_IDENTITIES.charlie.pubkey,
);
await profilePopover
.getByTestId(
`user-profile-popover-huddle-${TEST_IDENTITIES.charlie.pubkey}`,
)
.click();
await expect
.poll(() =>
page.evaluate(
() =>
(window.__BUZZ_E2E_COMMAND_LOG__ ?? []).find(
(entry) => entry.command === "start_huddle",
)?.payload,
),
)
.toMatchObject({ memberPubkeys: [TEST_IDENTITIES.charlie.pubkey] });
});
test("owned agent mention profile exposes message and huddle actions", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "online",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Can @charlie take this?", {
mentionPubkeys: [TEST_IDENTITIES.charlie.pubkey],
});
await waitForTimelineSettled(page);
const mentionChip = page
.getByTestId("message-row")
.filter({ hasText: "Can charlie take this?" })
.locator("[data-mention].agent-mention-highlight", { hasText: "charlie" });
await expect(mentionChip).toBeVisible();
await mentionChip.hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expectOwnedAgentProfileActions(
profilePopover,
TEST_IDENTITIES.charlie.pubkey,
);
});
test("profile popover wave sends a direct message for a human profile", async ({
page,
}) => {
await installMockBridge(page, { sendMessageDelayMs: 2_500 });
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Bob says hello.", {
pubkey: TEST_IDENTITIES.bob.pubkey,
});
await waitForTimelineSettled(page);
const bobMessage = page
.getByTestId("message-row")
.filter({ hasText: "Bob says hello." })
.first();
await bobMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-message-${TEST_IDENTITIES.bob.pubkey}`,
),
).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-huddle-${TEST_IDENTITIES.bob.pubkey}`,
),
).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-wave-${TEST_IDENTITIES.bob.pubkey}`,
),
).toBeVisible();
await profilePopover
.getByTestId(`user-profile-popover-wave-${TEST_IDENTITIES.bob.pubkey}`)
.click();
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
const waveAttachment = page.getByTestId("message-wave-attachment");
await expect(waveAttachment).toBeVisible({ timeout: 1_500 });
await expect(page.getByText("Sending")).toHaveCount(0, { timeout: 4_000 });
await waitForTimelineSettled(page);
await expect(waveAttachment).toContainText("👋");
await expect(waveAttachment).toContainText("npub1mock... waved at you.");
await expect(waveAttachment).toContainText("Start a huddle to talk to them.");
await expect(
waveAttachment.getByRole("button", { name: "Start huddle" }),
).toBeVisible();
const commandLog = await readCommandPayloadLog(page);
expect(commandLog).toEqual(
expect.arrayContaining([
expect.objectContaining({
command: "send_channel_message",
payload: expect.objectContaining({
content: expect.stringContaining("npub1mock... waved at you."),
}),
}),
]),
);
expect(commandLog).toEqual(
expect.arrayContaining([
expect.objectContaining({
command: "send_channel_message",
payload: expect.objectContaining({
content: expect.stringContaining("<!-- buzz:wave:v1 -->"),
}),
}),
]),
);
});
test("delayed inaccessible agent profile keeps all actions hidden", async ({
page,
}) => {
await installMockBridge(page, {
agentListDelayMs: 5_000,
relayAgents: [
{
pubkey: DELAYED_RELAY_AGENT_PUBKEY,
name: "orbit",
channelNames: ["general"],
},
],
searchProfiles: [
{
pubkey: DELAYED_RELAY_AGENT_PUBKEY,
displayName: "orbit",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Orbit checking in.", {
pubkey: DELAYED_RELAY_AGENT_PUBKEY,
});
await waitForTimelineSettled(page);
const orbitMessage = page
.getByTestId("message-row")
.filter({ hasText: "Orbit checking in." })
.first();
await orbitMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-message-${DELAYED_RELAY_AGENT_PUBKEY}`,
),
).toHaveCount(0);
await expect(
profilePopover.getByTestId(
`user-profile-popover-wave-${DELAYED_RELAY_AGENT_PUBKEY}`,
),
).toHaveCount(0);
await expect(
profilePopover.getByTestId(
`user-profile-popover-huddle-${DELAYED_RELAY_AGENT_PUBKEY}`,
),
).toHaveCount(0);
});