9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
2380 lines
74 KiB
TypeScript
2380 lines
74 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
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 entries =
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
|
command: string;
|
|
payload: unknown;
|
|
}>;
|
|
}
|
|
).__BUZZ_E2E_COMMAND_LOG__ ?? [];
|
|
|
|
for (const entry of entries) {
|
|
if (entry.command !== "plugin:websocket|send") continue;
|
|
const data = (
|
|
entry.payload as { message?: { data?: string } } | undefined
|
|
)?.message?.data;
|
|
if (!data) continue;
|
|
|
|
try {
|
|
const frame = JSON.parse(data) as [
|
|
string,
|
|
{ content?: string; tags?: string[][] },
|
|
];
|
|
if (frame[0] !== "EVENT" || frame[1]?.content !== 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("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("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).toBeEmpty();
|
|
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.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");
|
|
|
|
const dropdown = autocomplete(page);
|
|
await dropdown.getByText("bob").click();
|
|
|
|
await expect(input).toHaveText("Hey @bob ");
|
|
const mentionChip = input.locator(".mention-chip", {
|
|
hasText: "@bob",
|
|
});
|
|
await expect(mentionChip).toBeVisible();
|
|
await expect(mentionChip).not.toHaveClass(/agent-mention-highlight/);
|
|
});
|
|
|
|
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");
|
|
await input.fill("Hey @ali");
|
|
|
|
const dropdown = autocomplete(page);
|
|
await dropdown.getByText("alice").click();
|
|
|
|
await expect(input).toHaveText("Hey @alice ");
|
|
const agentMentionChip = input.locator(".agent-mention-highlight", {
|
|
hasText: "alice",
|
|
});
|
|
await expect(agentMentionChip).toBeVisible();
|
|
await expect(agentMentionChip).toHaveText("alice");
|
|
await expect(agentMentionChip).toHaveCSS("display", "inline-flex");
|
|
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 use the agent composer style", 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);
|
|
await expect(dropdown.getByText("charlie")).toBeVisible();
|
|
await expect(dropdown.getByText("agent")).toBeVisible();
|
|
await input.press("Enter");
|
|
|
|
const agentMentionChip = input.locator(".agent-mention-highlight", {
|
|
hasText: "charlie",
|
|
});
|
|
await expect(agentMentionChip).toBeVisible();
|
|
await expect(agentMentionChip).toHaveText("charlie");
|
|
});
|
|
|
|
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("relay-only allowlisted agents are visible in channel mentions", 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 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 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("relay-only anyone agents are visible when a channel is shared", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
relayAgents: [
|
|
{
|
|
pubkey: ALLOWLIST_RELAY_AGENT_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, {
|
|
managedAgents: [
|
|
{
|
|
pubkey: OUT_OF_CHANNEL_MANAGED_AGENT_PUBKEY,
|
|
name: "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("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",
|
|
);
|
|
|
|
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: "fizz" });
|
|
await expect(mentionChip).toBeVisible();
|
|
});
|
|
|
|
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("selecting a managed non-member agent from a DM inserts @Name into input", 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);
|
|
await expect(dropdown.getByText("charlie")).toBeVisible();
|
|
await expect(autocomplete(page)).toHaveCount(1);
|
|
await expect(input.locator(".mention-chip")).toHaveCount(0);
|
|
await input.press("Enter");
|
|
|
|
await expect(input).toHaveText("@charlie ");
|
|
await expect(input.locator(".mention-chip")).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.locator("svg")).toHaveCount(0);
|
|
});
|
|
|
|
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.locator(".mention-chip-prefix")).toHaveText("@");
|
|
await expect(mentionChip.locator("svg")).toHaveCount(0);
|
|
});
|
|
|
|
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: "Bumble",
|
|
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", "Bumble checking in.", {
|
|
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
|
|
});
|
|
await waitForTimelineSettled(page);
|
|
|
|
const bumbleMessage = page
|
|
.getByTestId("message-row")
|
|
.filter({ hasText: "Bumble checking in." })
|
|
.first();
|
|
await bumbleMessage.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: "Bumble",
|
|
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", "Bumble checking in.", {
|
|
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
|
|
});
|
|
await waitForTimelineSettled(page);
|
|
|
|
const bumbleMessage = page
|
|
.getByTestId("message-row")
|
|
.filter({ hasText: "Bumble checking in." })
|
|
.first();
|
|
await bumbleMessage.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: "Bumble",
|
|
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", "Bumble checking in.", {
|
|
pubkey: OWNED_AGENT_PROFILE_PUBKEY,
|
|
});
|
|
await waitForTimelineSettled(page);
|
|
|
|
const bumbleMessage = page
|
|
.getByTestId("message-row")
|
|
.filter({ hasText: "Bumble checking in." })
|
|
.first();
|
|
await bumbleMessage.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);
|
|
});
|