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>
2592 lines
86 KiB
TypeScript
2592 lines
86 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
|
||
import type { RelayEvent } from "@/shared/api/types";
|
||
|
||
import { emojiAvatarDataUrl } from "@/features/profile/ui/ProfileAvatarEditor.utils";
|
||
|
||
import { waitForAnimations } from "../helpers/animations";
|
||
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
||
|
||
function createCatalogEvent(input: {
|
||
ownerPubkey: string;
|
||
sourcePersonaId: string;
|
||
displayName: string;
|
||
systemPrompt: string;
|
||
createdAt?: number;
|
||
shared?: boolean;
|
||
avatarUrl?: string;
|
||
}): RelayEvent {
|
||
return {
|
||
id: "1".repeat(64),
|
||
pubkey: input.ownerPubkey,
|
||
created_at: input.createdAt ?? 1_721_750_400,
|
||
kind: 30175,
|
||
tags: [
|
||
["d", input.sourcePersonaId],
|
||
...(input.shared === false ? [] : [["shared", "true"]]),
|
||
],
|
||
content: JSON.stringify({
|
||
display_name: input.displayName,
|
||
system_prompt: input.systemPrompt,
|
||
avatar_url: input.avatarUrl ?? null,
|
||
runtime: null,
|
||
model: null,
|
||
provider: null,
|
||
name_pool: [],
|
||
}),
|
||
sig: "2".repeat(128),
|
||
};
|
||
}
|
||
|
||
test.beforeEach(async ({ page }) => {
|
||
await installMockBridge(page);
|
||
});
|
||
|
||
async function gotoApp(page: import("@playwright/test").Page) {
|
||
let lastError: unknown = null;
|
||
|
||
for (const attempt of [0, 1]) {
|
||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||
await waitForInvokeBridge(page);
|
||
|
||
try {
|
||
await expect(page.getByTestId("open-agents-view")).toBeVisible({
|
||
timeout: 10_000,
|
||
});
|
||
return;
|
||
} catch (error) {
|
||
lastError = error;
|
||
if (attempt === 1) {
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
|
||
throw lastError;
|
||
}
|
||
|
||
async function openPersonaCatalog(page: import("@playwright/test").Page) {
|
||
await page.getByTestId("new-agent-card").click();
|
||
}
|
||
|
||
async function getCatalogOrder(page: import("@playwright/test").Page) {
|
||
return page
|
||
.locator('[data-testid^="persona-catalog-list-item-"]')
|
||
.evaluateAll((elements) =>
|
||
elements.map((element) => element.getAttribute("data-testid") ?? ""),
|
||
);
|
||
}
|
||
|
||
async function selectCatalogPersona(
|
||
page: import("@playwright/test").Page,
|
||
personaId: string,
|
||
) {
|
||
await page.getByTestId(`persona-catalog-list-item-${personaId}`).click();
|
||
}
|
||
|
||
async function sharePersonaToCatalog(
|
||
page: import("@playwright/test").Page,
|
||
displayName: string,
|
||
) {
|
||
await page.getByLabel(`Open actions for ${displayName}`).click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
const catalogToggle = page.getByTestId("persona-share-catalog-access");
|
||
await expect(catalogToggle).not.toBeChecked();
|
||
await catalogToggle.click();
|
||
await expect(catalogToggle).toBeChecked();
|
||
await page
|
||
.getByTestId("persona-share-dialog")
|
||
.getByRole("button", { name: "Close" })
|
||
.click();
|
||
}
|
||
|
||
async function waitForInvokeBridge(page: import("@playwright/test").Page) {
|
||
await page.waitForFunction(
|
||
() => {
|
||
const tauriWindow = window as Window & {
|
||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
|
||
__TAURI_INTERNALS__?: {
|
||
invoke?: unknown;
|
||
};
|
||
};
|
||
|
||
return (
|
||
typeof tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ === "function" ||
|
||
typeof tauriWindow.__TAURI_INTERNALS__?.invoke === "function"
|
||
);
|
||
},
|
||
null,
|
||
{ timeout: 5_000 },
|
||
);
|
||
}
|
||
|
||
async function invokeTauri<T>(
|
||
page: import("@playwright/test").Page,
|
||
command: string,
|
||
payload?: Record<string, unknown>,
|
||
): Promise<T> {
|
||
await waitForInvokeBridge(page);
|
||
|
||
return page.evaluate(
|
||
async ({ command: targetCommand, payload: targetPayload }) => {
|
||
const tauriWindow = window as Window & {
|
||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
||
command: string,
|
||
payload?: Record<string, unknown>,
|
||
) => Promise<unknown>;
|
||
__TAURI_INTERNALS__?: {
|
||
invoke?: (
|
||
command: string,
|
||
payload?: Record<string, unknown>,
|
||
) => Promise<unknown>;
|
||
};
|
||
};
|
||
|
||
const invoke =
|
||
tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ??
|
||
tauriWindow.__TAURI_INTERNALS__?.invoke;
|
||
if (!invoke) {
|
||
throw new Error("Mock invoke bridge is unavailable.");
|
||
}
|
||
|
||
return (await invoke(targetCommand, targetPayload)) as T;
|
||
},
|
||
{ command, payload },
|
||
);
|
||
}
|
||
|
||
async function invokeTauriExpectError(
|
||
page: import("@playwright/test").Page,
|
||
command: string,
|
||
payload?: Record<string, unknown>,
|
||
) {
|
||
await waitForInvokeBridge(page);
|
||
|
||
return page.evaluate(
|
||
async ({ command: targetCommand, payload: targetPayload }) => {
|
||
const tauriWindow = window as Window & {
|
||
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
|
||
command: string,
|
||
payload?: Record<string, unknown>,
|
||
) => Promise<unknown>;
|
||
__TAURI_INTERNALS__?: {
|
||
invoke?: (
|
||
command: string,
|
||
payload?: Record<string, unknown>,
|
||
) => Promise<unknown>;
|
||
};
|
||
};
|
||
|
||
const invoke =
|
||
tauriWindow.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ??
|
||
tauriWindow.__TAURI_INTERNALS__?.invoke;
|
||
if (!invoke) {
|
||
throw new Error("Mock invoke bridge is unavailable.");
|
||
}
|
||
|
||
try {
|
||
await invoke(targetCommand, targetPayload);
|
||
return null;
|
||
} catch (error) {
|
||
return error instanceof Error ? error.message : String(error);
|
||
}
|
||
},
|
||
{ command, payload },
|
||
);
|
||
}
|
||
|
||
async function countCommandInvocations(
|
||
page: import("@playwright/test").Page,
|
||
command: string,
|
||
): Promise<number> {
|
||
return page.evaluate(
|
||
(targetCommand) =>
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_COMMANDS__?: string[];
|
||
}
|
||
).__BUZZ_E2E_COMMANDS__?.filter((invoked) => invoked === targetCommand)
|
||
.length ?? 0,
|
||
command,
|
||
);
|
||
}
|
||
|
||
test("catalog hides built-ins and shows the shared-agent empty state", async ({
|
||
page,
|
||
}) => {
|
||
await page.setViewportSize({ width: 1280, height: 420 });
|
||
await installMockBridge(page, {
|
||
activePersonaIds: ["builtin:fizz", "builtin:honey", "builtin:bumble"],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
await expect(page.getByTestId("agents-library-personas")).toBeVisible();
|
||
for (const personaName of ["Fizz", "Honey", "Bumble"]) {
|
||
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
||
personaName,
|
||
);
|
||
}
|
||
|
||
await openPersonaCatalog(page);
|
||
for (const personaName of ["Fizz", "Honey", "Bumble"]) {
|
||
await expect(page.getByTestId("persona-catalog-dialog")).not.toContainText(
|
||
personaName,
|
||
);
|
||
}
|
||
await expect(page.getByTestId("persona-catalog-dialog-header")).toBeVisible();
|
||
await expect(page.getByTestId("persona-catalog-dialog-body")).toBeVisible();
|
||
await expect(
|
||
page.getByText("No shared agents", { exact: true }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.locator('[data-testid^="persona-catalog-list-item-"]'),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
page.getByTestId("persona-catalog-use-agent-target"),
|
||
).toHaveCount(0);
|
||
|
||
await page
|
||
.getByTestId("persona-catalog-dialog")
|
||
.getByRole("button", { name: "Close" })
|
||
.click();
|
||
await page.getByLabel("Open actions for Fizz").click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
await expect(page.getByTestId("persona-share-catalog")).toHaveCount(0);
|
||
await expect(page.getByTestId("persona-share-catalog-access")).toHaveCount(0);
|
||
});
|
||
|
||
test("catalog empty state remains available after reopening", async ({
|
||
page,
|
||
}) => {
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await openPersonaCatalog(page);
|
||
await expect(
|
||
page.getByText("No shared agents", { exact: true }),
|
||
).toBeVisible();
|
||
|
||
await page
|
||
.getByTestId("persona-catalog-dialog")
|
||
.getByRole("button", { name: "Close" })
|
||
.click();
|
||
await expect(page.getByTestId("persona-catalog-dialog")).not.toBeVisible();
|
||
await openPersonaCatalog(page);
|
||
await expect(
|
||
page.getByText("No shared agents", { exact: true }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("built-in persona edits persist", async ({ page }) => {
|
||
await installMockBridge(page, {
|
||
activePersonaIds: ["builtin:fizz"],
|
||
globalAgentConfig: {
|
||
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
|
||
provider: "anthropic",
|
||
model: "claude-opus-4-5",
|
||
},
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
await page.getByLabel("Open actions for Fizz").click();
|
||
await page.getByRole("menuitem", { name: "Edit" }).click();
|
||
|
||
const dialog = page.getByTestId("persona-dialog");
|
||
await dialog.getByLabel("Agent name").fill("My Fizz");
|
||
await dialog.getByLabel("Agent instruction").fill("User-edited instructions");
|
||
await dialog.getByRole("button", { name: "Save changes" }).click();
|
||
|
||
await expect(dialog).toHaveCount(0);
|
||
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
||
"My Fizz",
|
||
);
|
||
const personas = await invokeTauri<
|
||
Array<{ id: string; display_name: string; system_prompt: string }>
|
||
>(page, "list_personas");
|
||
expect(
|
||
personas.find((persona) => persona.id === "builtin:fizz"),
|
||
).toMatchObject({
|
||
display_name: "My Fizz",
|
||
system_prompt: "User-edited instructions",
|
||
});
|
||
});
|
||
|
||
test("searches agent avatar emoji with focus on open", async ({ page }) => {
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByTestId("new-agent-card").click();
|
||
|
||
await expect(page.getByTestId("persona-dialog")).toBeVisible();
|
||
await page.getByLabel("Add avatar").click();
|
||
await page.getByRole("tab", { name: "Emoji" }).click();
|
||
|
||
const picker = page.locator("em-emoji-picker");
|
||
const searchInput = picker.locator("input[type='search']");
|
||
await expect(searchInput).toBeVisible();
|
||
await expect(searchInput).toBeFocused();
|
||
|
||
await searchInput.fill("rocket");
|
||
await picker.locator("button[aria-label='🚀']").first().click();
|
||
|
||
await expect(page.getByRole("img", { name: "Agent name avatar" })).toHaveText(
|
||
"🚀",
|
||
);
|
||
});
|
||
|
||
test("agent avatar emoji picker scrolls inside its popover", async ({
|
||
page,
|
||
}) => {
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByTestId("new-agent-card").click();
|
||
|
||
await expect(page.getByTestId("persona-dialog")).toBeVisible();
|
||
await page.getByLabel("Add avatar").click();
|
||
await page.getByRole("tab", { name: "Emoji" }).click();
|
||
await expect(page.locator("em-emoji-picker")).toBeVisible();
|
||
|
||
await page.waitForFunction(() => {
|
||
const picker = document.querySelector("em-emoji-picker");
|
||
const scroll = picker?.shadowRoot?.querySelector(".scroll");
|
||
return (
|
||
scroll instanceof HTMLElement && scroll.scrollHeight > scroll.clientHeight
|
||
);
|
||
});
|
||
|
||
const before = await page.locator("em-emoji-picker").evaluate((picker) => {
|
||
const scroll = picker.shadowRoot?.querySelector(".scroll");
|
||
return scroll instanceof HTMLElement ? scroll.scrollTop : -1;
|
||
});
|
||
|
||
const box = await page.locator("em-emoji-picker").boundingBox();
|
||
if (!box) {
|
||
throw new Error("Could not measure emoji picker bounds.");
|
||
}
|
||
await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2);
|
||
await page.mouse.wheel(0, 500);
|
||
|
||
await expect
|
||
.poll(async () =>
|
||
page.locator("em-emoji-picker").evaluate((picker) => {
|
||
const scroll = picker.shadowRoot?.querySelector(".scroll");
|
||
return scroll instanceof HTMLElement ? scroll.scrollTop : -1;
|
||
}),
|
||
)
|
||
.toBeGreaterThan(before);
|
||
});
|
||
|
||
test("the new agent card opens unified create, catalog, and import flows", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
activePersonaIds: ["builtin:fizz", "builtin:honey", "builtin:bumble"],
|
||
personas: [
|
||
{
|
||
id: "custom:code-reviewer",
|
||
displayName: "Code Reviewer",
|
||
systemPrompt: "Review code changes.",
|
||
},
|
||
{
|
||
id: "custom:layout-auditor",
|
||
displayName: "Layout Auditor",
|
||
systemPrompt: "Review responsive layouts.",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
const newAgentCard = page.getByTestId("new-agent-card");
|
||
await expect(newAgentCard).toHaveText("");
|
||
await expect(newAgentCard.locator(".lucide-plus")).toBeVisible();
|
||
|
||
const agentCards = page.locator(
|
||
'[data-testid^="persona-agent-row-"], [data-testid="new-agent-card"]',
|
||
);
|
||
await expect(agentCards.first()).toBeVisible();
|
||
const headerBox = await page
|
||
.getByRole("heading", { level: 1, name: "Agents" })
|
||
.locator("../..")
|
||
.boundingBox();
|
||
const cardBoxes = await agentCards.evaluateAll((cards) =>
|
||
cards.map((card) => {
|
||
const box = card.getBoundingClientRect();
|
||
return { left: box.left, right: box.right, top: box.top };
|
||
}),
|
||
);
|
||
const firstRowTop = Math.min(...cardBoxes.map(({ top }) => top));
|
||
expect(
|
||
cardBoxes.filter(({ top }) => Math.abs(top - firstRowTop) < 1),
|
||
).toHaveLength(5);
|
||
const rightmostFirstRowCard = Math.max(
|
||
...cardBoxes
|
||
.filter(({ top }) => Math.abs(top - firstRowTop) < 1)
|
||
.map(({ right }) => right),
|
||
);
|
||
const leftmostFirstRowCard = Math.min(
|
||
...cardBoxes
|
||
.filter(({ top }) => Math.abs(top - firstRowTop) < 1)
|
||
.map(({ left }) => left),
|
||
);
|
||
expect(headerBox).not.toBeNull();
|
||
expect(Math.abs((headerBox?.x ?? 0) - leftmostFirstRowCard)).toBeLessThan(1);
|
||
expect(rightmostFirstRowCard).toBeLessThanOrEqual(
|
||
(headerBox?.x ?? 0) + (headerBox?.width ?? 0) + 1,
|
||
);
|
||
|
||
await newAgentCard.click();
|
||
const catalogDialog = page.getByTestId("persona-catalog-dialog");
|
||
await expect(catalogDialog).toBeVisible();
|
||
await expect(page.getByTestId("agent-catalog-create")).toHaveAttribute(
|
||
"aria-current",
|
||
"true",
|
||
);
|
||
|
||
const dialog = page.getByTestId("persona-dialog");
|
||
await expect(dialog).toBeVisible();
|
||
await expect(
|
||
dialog.getByTestId("import-agent-snapshot-dialog-action"),
|
||
).toHaveCount(0);
|
||
await expect(dialog).not.toContainText("Enter a name for this agent.");
|
||
|
||
await page.getByTestId("agent-catalog-import").click();
|
||
await expect(page.getByTestId("agent-catalog-import-dropzone")).toBeVisible();
|
||
const fileChooserPromise = page.waitForEvent("filechooser");
|
||
await page.getByTestId("agent-catalog-import-dropzone").click();
|
||
const fileChooser = await fileChooserPromise;
|
||
await fileChooser.setFiles({
|
||
buffer: Buffer.from("{}"),
|
||
mimeType: "application/json",
|
||
name: "imported.agent.json",
|
||
});
|
||
await expect(page.getByTestId("agent-snapshot-import-dialog")).toBeVisible();
|
||
});
|
||
|
||
test("embedded create keeps its draft when discard is cancelled", async ({
|
||
page,
|
||
}) => {
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByTestId("new-agent-card").click();
|
||
|
||
const dialog = page.getByTestId("persona-dialog");
|
||
const name = dialog.locator("#persona-display-name");
|
||
const instructions = dialog.locator("#persona-system-prompt");
|
||
await name.fill("Draft Keeper");
|
||
await instructions.fill("Preserve this draft through confirmation.");
|
||
|
||
await dialog.getByRole("button", { name: "Cancel" }).click();
|
||
const discardDialog = page.getByTestId("discard-create-agent-dialog");
|
||
await expect(discardDialog).toBeVisible();
|
||
await discardDialog.getByRole("button", { name: "Keep editing" }).click();
|
||
|
||
await expect(dialog).toBeVisible();
|
||
await expect(name).toHaveValue("Draft Keeper");
|
||
await expect(instructions).toHaveValue(
|
||
"Preserve this draft through confirmation.",
|
||
);
|
||
});
|
||
|
||
test("the new team card offers create and import", async ({ page }) => {
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
const newTeamCard = page.getByTestId("new-team-card");
|
||
await expect(newTeamCard).toHaveText("");
|
||
await expect(newTeamCard.locator(".lucide-plus")).toBeVisible();
|
||
|
||
await newTeamCard.click();
|
||
await expect(
|
||
page.getByRole("menuitem", { exact: true, name: "Create team" }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByRole("menuitem", { exact: true, name: "Import" }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("team cards follow the agents grid alignment at compact widths", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: "custom:team-layout",
|
||
displayName: "Team layout agent",
|
||
systemPrompt: "A test agent for team layout alignment.",
|
||
},
|
||
],
|
||
teams: [
|
||
{
|
||
id: "team-layout",
|
||
name: "Team layout",
|
||
personaIds: ["custom:team-layout"],
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
const agentsContent = page.getByTestId("agents-page-content");
|
||
const firstAgentCard = page.getByTestId(
|
||
"persona-agent-row-custom:team-layout",
|
||
);
|
||
const firstTeamCard = page.getByTestId("team-card-team-layout");
|
||
const agentGrid = firstAgentCard.locator("xpath=..");
|
||
const teamGrid = firstTeamCard.locator("xpath=..");
|
||
const firstAgentGridCard = agentGrid.locator(":scope > *").first();
|
||
const firstTeamGridCard = teamGrid.locator(":scope > *").first();
|
||
|
||
await agentsContent.evaluate((element) => {
|
||
(element as HTMLElement).style.width = "650px";
|
||
});
|
||
const wideAgentBox = await firstAgentGridCard.boundingBox();
|
||
const wideTeamBox = await firstTeamGridCard.boundingBox();
|
||
expect(wideAgentBox).not.toBeNull();
|
||
expect(wideTeamBox).not.toBeNull();
|
||
expect(Math.abs((wideAgentBox?.x ?? 0) - (wideTeamBox?.x ?? 0))).toBeLessThan(
|
||
1,
|
||
);
|
||
|
||
await agentsContent.evaluate((element) => {
|
||
(element as HTMLElement).style.width = "600px";
|
||
});
|
||
const compactAgentBox = await firstAgentGridCard.boundingBox();
|
||
const compactTeamBox = await firstTeamGridCard.boundingBox();
|
||
expect(compactAgentBox?.width ?? 0).toBeLessThan(wideAgentBox?.width ?? 0);
|
||
expect(
|
||
Math.abs((compactAgentBox?.width ?? 0) - (compactTeamBox?.width ?? 0)),
|
||
).toBeLessThan(1);
|
||
expect(
|
||
Math.abs((compactAgentBox?.x ?? 0) - (compactTeamBox?.x ?? 0)),
|
||
).toBeLessThan(1);
|
||
});
|
||
|
||
test("team cards use the thread-style overlapping avatar stack", async ({
|
||
page,
|
||
}) => {
|
||
const personaIds = ["custom:design", "custom:build", "custom:ship"];
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
avatarUrl: "/onboarding/starter-team/fizz.png",
|
||
id: personaIds[0],
|
||
displayName: "Design",
|
||
systemPrompt: "You design interfaces.",
|
||
},
|
||
{
|
||
id: personaIds[1],
|
||
displayName: "Build",
|
||
systemPrompt: "You build interfaces.",
|
||
},
|
||
{
|
||
id: personaIds[2],
|
||
displayName: "Ship",
|
||
systemPrompt: "You ship interfaces.",
|
||
},
|
||
],
|
||
teams: [
|
||
{
|
||
name: "Product crew",
|
||
personaIds,
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
const stack = page.getByLabel("Product crew member avatars");
|
||
const avatars = stack.locator('[data-team-member-avatar="avatar"]');
|
||
await expect(avatars).toHaveCount(3);
|
||
await expect(avatars.nth(1)).toHaveClass(/-ml-5/);
|
||
await expect(avatars.nth(2)).toHaveClass(/-ml-5/);
|
||
|
||
const boxes = await avatars.evaluateAll((elements) =>
|
||
elements.map((element) => {
|
||
const box = element.getBoundingClientRect();
|
||
return { left: box.left, right: box.right };
|
||
}),
|
||
);
|
||
expect(boxes[1]?.left).toBeLessThan(boxes[0]?.right ?? 0);
|
||
expect(boxes[2]?.left).toBeLessThan(boxes[1]?.right ?? 0);
|
||
await expect(avatars.first()).not.toHaveCSS("mask-image", "none");
|
||
await expect(avatars.last()).toHaveCSS("mask-image", "none");
|
||
const avatarSurfaceStyles = await avatars
|
||
.locator(":scope > *")
|
||
.evaluateAll((elements) =>
|
||
elements.map((element) => {
|
||
const styles = getComputedStyle(element);
|
||
const hasVisibleShadow = [
|
||
...styles.boxShadow.matchAll(/rgba?\(([^)]+)\)/g),
|
||
].some((match) => {
|
||
if (match[0].startsWith("rgb(")) return true;
|
||
const channels = match[1]?.split(/[\s,/]+/).filter(Boolean) ?? [];
|
||
return Number(channels.at(-1)) > 0;
|
||
});
|
||
return {
|
||
borderWidth: styles.borderTopWidth,
|
||
hasVisibleShadow,
|
||
};
|
||
}),
|
||
);
|
||
expect(avatarSurfaceStyles).toEqual([
|
||
{ borderWidth: "0px", hasVisibleShadow: false },
|
||
{ borderWidth: "0px", hasVisibleShadow: false },
|
||
{ borderWidth: "0px", hasVisibleShadow: false },
|
||
]);
|
||
});
|
||
|
||
test("agent defaults stays in the header without an actions menu", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
acpRuntimesCatalog: [
|
||
{
|
||
auth_status: { status: "logged_in" },
|
||
availability: "available",
|
||
avatar_url: "",
|
||
binary_path: "/usr/local/bin/codex",
|
||
can_auto_install: false,
|
||
command: "codex",
|
||
default_args: [],
|
||
id: "codex",
|
||
install_hint: "",
|
||
install_instructions_url: "https://example.com",
|
||
label: "Codex",
|
||
login_hint: null,
|
||
mcp_command: null,
|
||
node_required: false,
|
||
underlying_cli_path: null,
|
||
},
|
||
],
|
||
globalAgentConfig: {
|
||
env_vars: {},
|
||
model: "gpt-5.5[high]",
|
||
preferred_runtime: "codex",
|
||
provider: null,
|
||
},
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
await expect(page.getByTestId("agent-header-actions-button")).toHaveCount(0);
|
||
await expect(
|
||
page.getByRole("menuitem", { name: "Import agent" }),
|
||
).toHaveCount(0);
|
||
|
||
const defaultsButton = page.getByTestId("agent-defaults-button");
|
||
await expect(defaultsButton).toHaveText("Agent defaults");
|
||
await defaultsButton.click();
|
||
const defaultsDialog = page.getByTestId("agent-ai-defaults-dialog");
|
||
await expect(defaultsDialog).toBeVisible();
|
||
await expect(
|
||
defaultsDialog.getByTestId("global-agent-default-harness"),
|
||
).toHaveAttribute("data-value", "codex");
|
||
await expect(
|
||
defaultsDialog.getByTestId("global-agent-default-harness"),
|
||
).toContainText("Codex");
|
||
await expect(
|
||
defaultsDialog.getByTestId("global-agent-model"),
|
||
).toHaveAttribute("data-value", "gpt-5.5[high]");
|
||
await expect(defaultsDialog.getByTestId("global-agent-model")).toContainText(
|
||
"gpt-5.5[high]",
|
||
);
|
||
await page.keyboard.press("Escape");
|
||
await expect(defaultsDialog).toHaveCount(0);
|
||
});
|
||
|
||
test("unconfigured agent defaults use the setup label", async ({ page }) => {
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
await expect(page.getByTestId("agent-defaults-button")).toHaveText(
|
||
"Set agent defaults",
|
||
);
|
||
});
|
||
|
||
test("moves agent actions into an overflow menu in a narrow view", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: "custom:compact-actions",
|
||
displayName: "Compact actions agent",
|
||
isActive: true,
|
||
systemPrompt: "A test agent for compact header actions.",
|
||
},
|
||
],
|
||
managedAgents: [
|
||
{
|
||
name: "Compact actions instance",
|
||
personaId: "custom:compact-actions",
|
||
pubkey: "cd".repeat(32),
|
||
status: "running",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByTestId("agents-page-content").evaluate((element) => {
|
||
(element as HTMLElement).style.width = "650px";
|
||
});
|
||
|
||
await expect(page.getByTestId("agent-defaults-button")).toBeVisible();
|
||
await expect(
|
||
page.getByText("Set up and manage your agents.", { exact: true }),
|
||
).toHaveJSProperty("scrollHeight", 24);
|
||
|
||
await page.getByTestId("agents-page-content").evaluate((element) => {
|
||
(element as HTMLElement).style.width = "600px";
|
||
});
|
||
await expect(page.getByTestId("agent-defaults-button")).toBeHidden();
|
||
await page.getByTestId("agent-actions-menu-trigger").click();
|
||
await expect(
|
||
page.getByRole("menuitem", { name: "Set agent defaults" }),
|
||
).toBeVisible();
|
||
await expect(
|
||
page.getByRole("menuitem", { name: "Stop running agents" }),
|
||
).toBeVisible();
|
||
|
||
await page.getByRole("menuitem", { name: "Set agent defaults" }).click();
|
||
await expect(page.getByTestId("agent-ai-defaults-dialog")).toBeVisible();
|
||
|
||
await page.getByTestId("agents-page-content").evaluate((element) => {
|
||
(element as HTMLElement).style.width = "650px";
|
||
});
|
||
await expect(page.getByTestId("agent-defaults-button")).toBeVisible();
|
||
await page.keyboard.press("Escape");
|
||
await expect(page.getByTestId("agent-ai-defaults-dialog")).toHaveCount(0);
|
||
await expect(page.getByTestId("agent-defaults-button")).toBeFocused();
|
||
});
|
||
|
||
test("agent catalog chooser order stays stable when selection changes", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: "custom:builder",
|
||
displayName: "Builder",
|
||
systemPrompt: "Build the requested change.",
|
||
},
|
||
{
|
||
id: "custom:reviewer",
|
||
displayName: "Reviewer",
|
||
systemPrompt: "Review the requested change.",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await sharePersonaToCatalog(page, "Builder");
|
||
await sharePersonaToCatalog(page, "Reviewer");
|
||
await openPersonaCatalog(page);
|
||
|
||
const before = await getCatalogOrder(page);
|
||
await selectCatalogPersona(page, "custom:reviewer");
|
||
expect(await getCatalogOrder(page)).toEqual(before);
|
||
});
|
||
|
||
test("catalog detail pane shows the full persona details", async ({ page }) => {
|
||
const personaId = "custom:researcher";
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: personaId,
|
||
displayName: "Researcher",
|
||
systemPrompt: "Research the question and cite the evidence.",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await sharePersonaToCatalog(page, "Researcher");
|
||
await openPersonaCatalog(page);
|
||
|
||
await selectCatalogPersona(page, personaId);
|
||
const useAgentTarget = page.getByTestId(
|
||
`persona-catalog-use-agent-target-${personaId}`,
|
||
);
|
||
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Researcher",
|
||
);
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Added by You",
|
||
);
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Research the question and cite the evidence.",
|
||
);
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Custom agent",
|
||
);
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Preferred model",
|
||
);
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Preferred runtime",
|
||
);
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Agent instruction",
|
||
);
|
||
await expect(useAgentTarget).toHaveAttribute(
|
||
"aria-label",
|
||
"Researcher is already in My Agents",
|
||
);
|
||
await expect(useAgentTarget).toHaveText("Added to My Agents");
|
||
await expect(useAgentTarget).toBeDisabled();
|
||
});
|
||
|
||
type AgentShareCommand = { command: string; payload: unknown };
|
||
|
||
async function openSafetyShareDialog(
|
||
page: import("@playwright/test").Page,
|
||
options: Parameters<typeof installMockBridge>[1] = {},
|
||
) {
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: "custom:safety-auditor",
|
||
displayName: "Safety Auditor",
|
||
systemPrompt: "You audit safety boundaries.",
|
||
},
|
||
],
|
||
searchProfiles: [
|
||
{
|
||
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||
displayName: "Charlie",
|
||
},
|
||
],
|
||
uploadDescriptors: [
|
||
{
|
||
url: `https://mock.relay/media/${"d".repeat(64)}.png`,
|
||
sha256: "d".repeat(64),
|
||
size: 1234,
|
||
type: "image/png",
|
||
uploaded: Math.floor(Date.now() / 1000),
|
||
filename: "safety-auditor.agent.png",
|
||
},
|
||
],
|
||
...options,
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByLabel("Open actions for Safety Auditor").click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
await expect(page.getByTestId("persona-share-dialog")).toBeVisible();
|
||
}
|
||
|
||
async function selectCharlieRecipient(page: import("@playwright/test").Page) {
|
||
const search = page.getByTestId("persona-share-recipient-search");
|
||
await expect(search).toBeEnabled({ timeout: 5_000 });
|
||
await search.fill("charlie");
|
||
await page
|
||
.getByTestId(
|
||
`persona-share-recipient-option-${TEST_IDENTITIES.charlie.pubkey}`,
|
||
)
|
||
.click();
|
||
}
|
||
|
||
async function readAgentShareCommands(
|
||
page: import("@playwright/test").Page,
|
||
): Promise<AgentShareCommand[]> {
|
||
return page.evaluate(
|
||
() =>
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_COMMAND_LOG__?: AgentShareCommand[];
|
||
}
|
||
).__BUZZ_E2E_COMMAND_LOG__ ?? [],
|
||
);
|
||
}
|
||
|
||
test("custom personas share with people and keep export separate", async ({
|
||
page,
|
||
}) => {
|
||
const sharedAgentUrl = `https://mock.relay/media/${"b".repeat(64)}.png`;
|
||
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: "custom:analyst",
|
||
displayName: "Animation Auditor",
|
||
systemPrompt: "You audit animations.",
|
||
},
|
||
],
|
||
searchProfiles: [
|
||
{
|
||
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||
displayName: "Charlie",
|
||
},
|
||
{
|
||
pubkey: TEST_IDENTITIES.bob.pubkey,
|
||
displayName: "Bob",
|
||
},
|
||
...Array.from({ length: 12 }, (_, index) => ({
|
||
pubkey: (index + 10).toString(16).padStart(64, "0"),
|
||
displayName: `Person ${index + 1}`,
|
||
})),
|
||
],
|
||
uploadDescriptors: [
|
||
{
|
||
url: sharedAgentUrl,
|
||
sha256: "b".repeat(64),
|
||
size: 1234,
|
||
type: "image/png",
|
||
uploaded: Math.floor(Date.now() / 1000),
|
||
filename: "analyst.agent.png",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
||
"Animation Auditor",
|
||
);
|
||
|
||
const actionsButton = page.getByLabel("Open actions for Animation Auditor");
|
||
await expect(actionsButton.locator("svg")).toHaveClass(
|
||
/lucide-ellipsis-vertical/,
|
||
);
|
||
await actionsButton.click();
|
||
await expect(page.getByRole("menuitem")).toHaveText([
|
||
"Edit",
|
||
"Duplicate",
|
||
"Share",
|
||
"Delete",
|
||
]);
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
|
||
const shareDialog = page.getByTestId("persona-share-dialog");
|
||
await expect(shareDialog).toBeVisible();
|
||
const emptyRecipientSearch = shareDialog.getByTestId(
|
||
"persona-share-recipient-search",
|
||
);
|
||
const [searchIconColor, searchPlaceholderColor, searchInputColor] =
|
||
await Promise.all([
|
||
shareDialog
|
||
.locator("svg.lucide-search")
|
||
.evaluate((element) => getComputedStyle(element).color),
|
||
emptyRecipientSearch.evaluate(
|
||
(element) => getComputedStyle(element, "::placeholder").color,
|
||
),
|
||
emptyRecipientSearch.evaluate(
|
||
(element) => getComputedStyle(element).color,
|
||
),
|
||
]);
|
||
expect(searchIconColor).toBe(searchPlaceholderColor);
|
||
expect(searchPlaceholderColor).not.toBe(searchInputColor);
|
||
const shareCloseButton = shareDialog.getByRole("button", { name: "Close" });
|
||
expect(
|
||
await shareCloseButton.evaluate(
|
||
(button) => getComputedStyle(button).boxShadow,
|
||
),
|
||
).not.toContain("1px");
|
||
await expect(
|
||
page.getByRole("heading", { name: "Share Animation Auditor" }),
|
||
).toBeVisible();
|
||
await expect(shareDialog.getByText("Added by You")).toHaveCount(0);
|
||
const shareDescription = shareDialog.getByTestId(
|
||
"persona-share-share-description",
|
||
);
|
||
await expect(shareDescription).toHaveText(
|
||
"Anyone you share this agent with will receive a copy they can add and use. Changes you make later won’t sync.",
|
||
);
|
||
await expect(shareDescription).toHaveClass(/text-sm.*text-muted-foreground/);
|
||
const shareDescriptionId = await shareDescription.getAttribute("id");
|
||
expect(shareDescriptionId).toBeTruthy();
|
||
await expect(shareDialog).toHaveAttribute(
|
||
"aria-describedby",
|
||
shareDescriptionId ?? "",
|
||
);
|
||
const shareDescriptionMetrics = await shareDescription.evaluate(
|
||
(element) => ({
|
||
height: element.getBoundingClientRect().height,
|
||
lineHeight: Number.parseFloat(getComputedStyle(element).lineHeight),
|
||
}),
|
||
);
|
||
expect(shareDescriptionMetrics.height).toBeLessThanOrEqual(
|
||
shareDescriptionMetrics.lineHeight * 2 + 1,
|
||
);
|
||
await expect(
|
||
shareDialog.getByRole("heading", { name: "Who has access" }),
|
||
).toHaveCount(0);
|
||
await expect(shareDialog.getByText("Owner", { exact: true })).toHaveCount(0);
|
||
await expect(shareDialog.getByText("(You)", { exact: true })).toHaveCount(0);
|
||
const linkRow = page.getByTestId("persona-share-link-row");
|
||
await expect(page.getByTestId("persona-share-copy-link")).toBeVisible();
|
||
await expect(
|
||
shareDialog.getByRole("heading", { name: "Share with a link" }),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
shareDialog.getByText("Anyone with the link can add and use a copy."),
|
||
).toHaveCount(0);
|
||
await expect(page.getByTestId("persona-share-send")).toHaveCount(0);
|
||
const copyLinkButton = page.getByTestId("persona-share-copy-link");
|
||
const catalogSection = page.getByTestId("persona-share-catalog");
|
||
const shareMainCard = page.getByTestId("persona-share-main-card");
|
||
const exportAgentRow = page.getByTestId("persona-share-export");
|
||
await waitForAnimations(page);
|
||
const [
|
||
linkRowBox,
|
||
initialCopyLinkButtonBox,
|
||
catalogSectionBox,
|
||
shareMainCardBox,
|
||
exportAgentRowBox,
|
||
] = await Promise.all([
|
||
linkRow.boundingBox(),
|
||
copyLinkButton.boundingBox(),
|
||
catalogSection.boundingBox(),
|
||
shareMainCard.boundingBox(),
|
||
exportAgentRow.boundingBox(),
|
||
]);
|
||
const shareDescriptionBox = await shareDescription.boundingBox();
|
||
const recipientFieldBox = await page
|
||
.getByTestId("persona-share-recipient-field")
|
||
.boundingBox();
|
||
// Without memories, the recipient field flows directly into the link action.
|
||
expect(recipientFieldBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(shareDescriptionBox?.y ?? 0) + (shareDescriptionBox?.height ?? 0),
|
||
);
|
||
await expect(page.getByTestId("persona-share-link-settings")).toHaveCount(0);
|
||
await expect(page.getByTestId("persona-share-share-level-row")).toHaveCount(
|
||
0,
|
||
);
|
||
expect(linkRowBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(recipientFieldBox?.y ?? 0) + (recipientFieldBox?.height ?? 0),
|
||
);
|
||
// Copy link stays inside the main card, after the shared settings divider, while catalog and
|
||
// export are separate rows below it.
|
||
expect(initialCopyLinkButtonBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
linkRowBox?.y ?? 0,
|
||
);
|
||
expect(
|
||
(initialCopyLinkButtonBox?.y ?? 0) +
|
||
(initialCopyLinkButtonBox?.height ?? 0),
|
||
).toBeLessThanOrEqual((linkRowBox?.y ?? 0) + (linkRowBox?.height ?? 0) + 1);
|
||
await expect(page.getByTestId("persona-share-link-divider")).toHaveClass(
|
||
/bg-input\/40/,
|
||
);
|
||
expect(catalogSectionBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(shareMainCardBox?.y ?? 0) + (shareMainCardBox?.height ?? 0) + 12,
|
||
);
|
||
expect(exportAgentRowBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(catalogSectionBox?.y ?? 0) + (catalogSectionBox?.height ?? 0) + 12,
|
||
);
|
||
await expect(copyLinkButton).toHaveClass(
|
||
/border.*bg-background.*border-border/,
|
||
);
|
||
const copyLinkHasVisibleShadow = () =>
|
||
copyLinkButton.evaluate((element) => {
|
||
const boxShadow = getComputedStyle(element).boxShadow;
|
||
if (boxShadow === "none") return false;
|
||
return [...boxShadow.matchAll(/rgba?\(([^)]+)\)/g)].some((match) => {
|
||
if (match[0].startsWith("rgb(")) return true;
|
||
const channels = match[1]?.split(/[\s,/]+/).filter(Boolean) ?? [];
|
||
return Number(channels.at(-1)) > 0;
|
||
});
|
||
});
|
||
await expect.poll(copyLinkHasVisibleShadow).toBe(false);
|
||
await copyLinkButton.hover();
|
||
await expect.poll(copyLinkHasVisibleShadow).toBe(false);
|
||
await expect(page.getByTestId("persona-share-share-level")).toHaveCount(0);
|
||
await expect(page.getByTestId("persona-share-recipient-access")).toHaveCount(
|
||
0,
|
||
);
|
||
await expect(page.getByTestId("persona-share-link-access")).toHaveCount(0);
|
||
await expect(
|
||
shareDialog.getByLabel("What to include in the link"),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
shareDialog.getByLabel("What to include", { exact: true }),
|
||
).toHaveCount(0);
|
||
await expect(shareDialog.getByText("Memories", { exact: true })).toHaveCount(
|
||
0,
|
||
);
|
||
await expect(
|
||
shareDialog.getByText("File format", { exact: true }),
|
||
).toHaveCount(0);
|
||
await expect(exportAgentRow).toHaveText("Export agent");
|
||
await expect(shareMainCard.getByTestId("persona-share-export")).toHaveCount(
|
||
0,
|
||
);
|
||
const [shareMainCardStyles, exportAgentRowStyles] = await Promise.all([
|
||
shareMainCard.evaluate((element) => ({
|
||
borderRadius: getComputedStyle(element).borderRadius,
|
||
boxShadow: getComputedStyle(element).boxShadow,
|
||
})),
|
||
exportAgentRow.evaluate((element) => ({
|
||
borderRadius: getComputedStyle(element).borderRadius,
|
||
boxShadow: getComputedStyle(element).boxShadow,
|
||
})),
|
||
]);
|
||
expect(exportAgentRowStyles.borderRadius).toBe(
|
||
shareMainCardStyles.borderRadius,
|
||
);
|
||
const shareMainCardShadow = shareMainCardStyles.boxShadow;
|
||
const exportAgentRowShadow = exportAgentRowStyles.boxShadow;
|
||
expect(exportAgentRowShadow).toBe(shareMainCardShadow);
|
||
expect(exportAgentRowShadow).not.toBe("none");
|
||
await expect(exportAgentRow).toHaveCSS("position", "relative");
|
||
expect(exportAgentRowBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(shareMainCardBox?.y ?? 0) + (shareMainCardBox?.height ?? 0) + 12,
|
||
);
|
||
await expect(page.getByTestId("agent-snapshot-export-dialog")).toHaveCount(0);
|
||
|
||
await exportAgentRow.click();
|
||
await expect(shareDialog).toHaveCount(0);
|
||
const exportDialog = page.getByTestId("agent-snapshot-export-dialog");
|
||
await expect(exportDialog).toBeVisible();
|
||
const exportDialogBox = await exportDialog.boundingBox();
|
||
expect(exportDialogBox?.width).toBeLessThanOrEqual(448);
|
||
await expect(
|
||
exportDialog.getByRole("heading", {
|
||
name: "Export Animation Auditor",
|
||
exact: true,
|
||
}),
|
||
).toBeVisible();
|
||
await expect(exportDialog.getByText("portable snapshot")).toHaveCount(0);
|
||
await expect(
|
||
exportDialog.getByRole("button", { name: "Send in Buzz" }),
|
||
).toHaveCount(0);
|
||
await expect(exportDialog.getByLabel("Memories")).toHaveCount(0);
|
||
await expect(
|
||
exportDialog.getByTestId("agent-snapshot-memory-value"),
|
||
).toHaveText("Agent only");
|
||
await expect(exportDialog.getByText("Start this agent")).toHaveCount(0);
|
||
await expect(
|
||
exportDialog.getByTestId("agent-snapshot-memory-value").locator("svg"),
|
||
).toHaveCount(0);
|
||
const formatTrigger = exportDialog.getByLabel("File format");
|
||
await expect(formatTrigger).toHaveText("PNG");
|
||
expect((await formatTrigger.boundingBox())?.width).toBeLessThan(80);
|
||
expect(await formatTrigger.evaluate((element) => element.tagName)).toBe(
|
||
"BUTTON",
|
||
);
|
||
await formatTrigger.click();
|
||
await expect(page.getByRole("menuitemradio", { name: "JSON" })).toBeVisible();
|
||
await page.getByRole("menuitemradio", { name: "PNG" }).click();
|
||
const exportFooter = exportDialog.getByTestId("agent-snapshot-export-footer");
|
||
const cancelButtonBox = await exportFooter
|
||
.getByRole("button", { name: "Cancel" })
|
||
.boundingBox();
|
||
const exportButtonBox = await exportFooter
|
||
.getByRole("button", { name: "Export" })
|
||
.boundingBox();
|
||
expect(cancelButtonBox?.x).toBeLessThan(exportButtonBox?.x ?? 0);
|
||
expect(
|
||
(exportButtonBox?.x ?? 0) -
|
||
((cancelButtonBox?.x ?? 0) + (cancelButtonBox?.width ?? 0)),
|
||
).toBeLessThanOrEqual(12);
|
||
await exportDialog.getByRole("button", { name: "Cancel" }).click();
|
||
|
||
await actionsButton.click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
await expect(shareDialog).toBeVisible();
|
||
await page.getByTestId("persona-share-copy-link").click();
|
||
await expect
|
||
.poll(() => page.evaluate(() => navigator.clipboard.readText()))
|
||
.toBe(sharedAgentUrl);
|
||
|
||
const copiedAgent = await page.evaluate(() => {
|
||
const commands =
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
||
command: string;
|
||
payload: { html?: string; text?: string };
|
||
}>;
|
||
}
|
||
).__BUZZ_E2E_COMMAND_LOG__ ?? [];
|
||
return commands.findLast(
|
||
(entry) => entry.command === "copy_text_to_clipboard",
|
||
)?.payload;
|
||
});
|
||
expect(copiedAgent?.text).toBe(sharedAgentUrl);
|
||
expect(copiedAgent?.html).toContain("data-buzz-agent-snapshot");
|
||
|
||
await page.keyboard.press("Escape");
|
||
await expect(shareDialog).toHaveCount(0);
|
||
await page.getByTestId("channel-general").click();
|
||
await page.evaluate(async ({ html, text }) => {
|
||
await navigator.clipboard.write([
|
||
new ClipboardItem({
|
||
"text/html": new Blob([html ?? ""], { type: "text/html" }),
|
||
"text/plain": new Blob([text ?? ""], { type: "text/plain" }),
|
||
}),
|
||
]);
|
||
}, copiedAgent ?? {});
|
||
await page
|
||
.getByTestId("message-composer")
|
||
.locator("[contenteditable='true']")
|
||
.click();
|
||
await page.keyboard.press("ControlOrMeta+V");
|
||
const composerAgentCard = page.getByTestId("composer-agent-snapshot-card");
|
||
await expect(composerAgentCard).toBeVisible();
|
||
await expect(composerAgentCard).toContainText("Animation Auditor");
|
||
const composerRemoveButton = page.getByTestId(
|
||
"composer-agent-snapshot-remove",
|
||
);
|
||
await expect(composerRemoveButton).toHaveCSS("border-top-width", "0px");
|
||
await composerRemoveButton.focus();
|
||
await page.keyboard.press("Tab");
|
||
await page.keyboard.press("Shift+Tab");
|
||
await expect(composerRemoveButton).toBeFocused();
|
||
expect(
|
||
await composerRemoveButton.evaluate(
|
||
(button) => getComputedStyle(button).boxShadow,
|
||
),
|
||
).not.toContain("1px");
|
||
await expect(composerRemoveButton).not.toHaveCSS(
|
||
"background-color",
|
||
"rgba(0, 0, 0, 0)",
|
||
);
|
||
await expect(page.getByTestId("send-message")).toBeEnabled();
|
||
await page.getByTestId("send-message").click();
|
||
|
||
const pastedAgentCard = page.getByTestId("agent-snapshot-card").last();
|
||
await expect(pastedAgentCard).toBeVisible();
|
||
await expect(pastedAgentCard).toContainText("Animation Auditor");
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await actionsButton.click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
await expect(shareDialog).toBeVisible();
|
||
|
||
const recipientSearch = page.getByTestId("persona-share-recipient-search");
|
||
const recipientField = page.getByTestId("persona-share-recipient-field");
|
||
await waitForAnimations(page);
|
||
const emptyRecipientFieldWidth =
|
||
(await recipientField.boundingBox())?.width ?? 0;
|
||
await recipientSearch.click();
|
||
const recipientPopover = page.getByTestId("persona-share-recipient-popover");
|
||
await expect(recipientPopover).toBeVisible();
|
||
await page.evaluate(() => {
|
||
const content = document.querySelector(
|
||
'[data-testid="persona-share-recipient-popover"]',
|
||
);
|
||
if (!content) throw new Error("Recipient popover is not mounted");
|
||
|
||
const stateChanges: string[] = [];
|
||
const observer = new MutationObserver(() => {
|
||
stateChanges.push(content.getAttribute("data-state") ?? "unmounted");
|
||
});
|
||
observer.observe(content, {
|
||
attributeFilter: ["data-state"],
|
||
attributes: true,
|
||
});
|
||
(
|
||
window as Window & {
|
||
__BUZZ_RECIPIENT_POPOVER_OBSERVER__?: MutationObserver;
|
||
__BUZZ_RECIPIENT_POPOVER_STATE_CHANGES__?: string[];
|
||
}
|
||
).__BUZZ_RECIPIENT_POPOVER_OBSERVER__ = observer;
|
||
(
|
||
window as Window & {
|
||
__BUZZ_RECIPIENT_POPOVER_STATE_CHANGES__?: string[];
|
||
}
|
||
).__BUZZ_RECIPIENT_POPOVER_STATE_CHANGES__ = stateChanges;
|
||
});
|
||
await recipientSearch.click();
|
||
await waitForAnimations(page);
|
||
const recipientPopoverStateChanges = await page.evaluate(() => {
|
||
const trackedWindow = window as Window & {
|
||
__BUZZ_RECIPIENT_POPOVER_OBSERVER__?: MutationObserver;
|
||
__BUZZ_RECIPIENT_POPOVER_STATE_CHANGES__?: string[];
|
||
};
|
||
trackedWindow.__BUZZ_RECIPIENT_POPOVER_OBSERVER__?.disconnect();
|
||
return trackedWindow.__BUZZ_RECIPIENT_POPOVER_STATE_CHANGES__ ?? [];
|
||
});
|
||
expect(recipientPopoverStateChanges).not.toContain("closed");
|
||
const recipientList = page.getByTestId("persona-share-recipient-results");
|
||
await expect(recipientList).toBeVisible();
|
||
await expect
|
||
.poll(() =>
|
||
recipientList.evaluate(
|
||
(element) => element.scrollHeight - element.clientHeight,
|
||
),
|
||
)
|
||
.toBeGreaterThan(0);
|
||
const recipientListBox = await recipientList.boundingBox();
|
||
expect(recipientListBox).not.toBeNull();
|
||
if (!recipientListBox) return;
|
||
await page.mouse.move(
|
||
recipientListBox.x + recipientListBox.width / 2,
|
||
recipientListBox.y + recipientListBox.height / 2,
|
||
);
|
||
await page.mouse.wheel(0, 300);
|
||
await expect
|
||
.poll(() => recipientList.evaluate((element) => element.scrollTop))
|
||
.toBeGreaterThan(0);
|
||
await recipientSearch.fill("charlie");
|
||
await page
|
||
.getByTestId(
|
||
`persona-share-recipient-option-${TEST_IDENTITIES.charlie.pubkey}`,
|
||
)
|
||
.click();
|
||
const charlieChip = page.getByTestId(
|
||
`persona-share-recipient-chip-${TEST_IDENTITIES.charlie.pubkey}`,
|
||
);
|
||
await expect(charlieChip).toBeVisible();
|
||
await expect(
|
||
page.getByRole("button", { name: "Verify charlie public key" }),
|
||
).toHaveCount(0);
|
||
await expect(recipientSearch).toHaveValue("");
|
||
await expect(recipientSearch).toHaveAttribute("placeholder", "");
|
||
await expect(
|
||
page
|
||
.getByTestId("persona-share-recipient-field")
|
||
.locator("svg.lucide-search"),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
page
|
||
.getByTestId("persona-share-recipient-field")
|
||
.getByTestId("persona-share-recipient-access"),
|
||
).toHaveCount(0);
|
||
await expect(page.getByTestId("persona-share-send")).toBeVisible();
|
||
|
||
await recipientSearch.fill("bob");
|
||
await page
|
||
.getByTestId(`persona-share-recipient-option-${TEST_IDENTITIES.bob.pubkey}`)
|
||
.click();
|
||
const bobChip = page.getByTestId(
|
||
`persona-share-recipient-chip-${TEST_IDENTITIES.bob.pubkey}`,
|
||
);
|
||
await expect(bobChip).toBeVisible();
|
||
await expect(bobChip).not.toHaveClass(/buzz-poof-trigger/);
|
||
await waitForAnimations(page);
|
||
const bobChipBox = await bobChip.boundingBox();
|
||
expect(bobChipBox).not.toBeNull();
|
||
if (!bobChipBox) return;
|
||
const removePointer = {
|
||
x: bobChipBox.x + bobChipBox.width - 3,
|
||
y: bobChipBox.y + bobChipBox.height / 2,
|
||
};
|
||
await page.mouse.move(removePointer.x, removePointer.y);
|
||
await page.mouse.down();
|
||
await expect(page.locator(".buzz-poof-burst")).toHaveCount(0);
|
||
await page.mouse.up();
|
||
await expect(bobChip).toHaveCount(0);
|
||
|
||
await waitForAnimations(page);
|
||
const selectedRecipientFieldWidth =
|
||
(await recipientField.boundingBox())?.width ?? 0;
|
||
const charlieChipBox = await charlieChip.boundingBox();
|
||
expect(charlieChipBox).not.toBeNull();
|
||
if (!charlieChipBox) return;
|
||
await page.mouse.move(
|
||
charlieChipBox.x + charlieChipBox.width / 2,
|
||
charlieChipBox.y + charlieChipBox.height / 2,
|
||
);
|
||
await page.mouse.down();
|
||
const lastChipRemovalSamples = await page.evaluate(async () => {
|
||
const samples: Array<{ elapsed: number; fieldWidth: number }> = [];
|
||
const start = performance.now();
|
||
|
||
await new Promise<void>((resolve) => {
|
||
const sample = (now: number) => {
|
||
const field = document.querySelector<HTMLElement>(
|
||
'[data-testid="persona-share-recipient-field"]',
|
||
);
|
||
samples.push({
|
||
elapsed: now - start,
|
||
fieldWidth: field?.getBoundingClientRect().width ?? 0,
|
||
});
|
||
if (now - start >= 240) {
|
||
resolve();
|
||
return;
|
||
}
|
||
requestAnimationFrame(sample);
|
||
};
|
||
requestAnimationFrame(sample);
|
||
});
|
||
|
||
return samples;
|
||
});
|
||
await page.mouse.up();
|
||
await expect(charlieChip).toHaveCount(0);
|
||
const firstExpandedSample = lastChipRemovalSamples.find(
|
||
({ fieldWidth }) => fieldWidth > selectedRecipientFieldWidth + 1,
|
||
);
|
||
expect(firstExpandedSample?.elapsed).toBeLessThan(100);
|
||
expect(
|
||
Math.abs(
|
||
(lastChipRemovalSamples.at(-1)?.fieldWidth ?? 0) -
|
||
emptyRecipientFieldWidth,
|
||
),
|
||
).toBeLessThanOrEqual(1);
|
||
|
||
await recipientSearch.fill("bob");
|
||
await page
|
||
.getByTestId(`persona-share-recipient-option-${TEST_IDENTITIES.bob.pubkey}`)
|
||
.click();
|
||
await page.getByTestId("persona-share-send").click();
|
||
await expect(
|
||
page.getByText("Sent a copy of Animation Auditor"),
|
||
).toBeVisible();
|
||
|
||
const sentAgentMessages = await page.evaluate(() =>
|
||
(
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
||
command: string;
|
||
payload: { content?: string };
|
||
}>;
|
||
}
|
||
).__BUZZ_E2E_COMMAND_LOG__ ?? []
|
||
).filter((entry) => entry.command === "send_channel_message"),
|
||
);
|
||
expect(sentAgentMessages.at(-1)?.payload.content).toBe(
|
||
`\n[Animation Auditor](${sharedAgentUrl})`,
|
||
);
|
||
await expect(shareDialog).toHaveCount(0);
|
||
});
|
||
|
||
test("custom personas can be shared to the relay catalog", async ({ page }) => {
|
||
const personaId = "custom:catalog-analyst";
|
||
await installMockBridge(page, {
|
||
globalAgentConfig: {
|
||
env_vars: { ANTHROPIC_API_KEY: "sk-ant-test" },
|
||
provider: "anthropic",
|
||
model: "claude-opus-4-5",
|
||
},
|
||
personas: [
|
||
{
|
||
id: personaId,
|
||
displayName: "Catalog Analyst",
|
||
respondTo: "allowlist",
|
||
respondToAllowlist: [TEST_IDENTITIES.alice.pubkey],
|
||
systemPrompt: `## Design System And Styling
|
||
|
||
- For design-system changes, check the local guidance in \`DESIGN.md\`, \`docs/color-token-mapping.md\`, \`src/shared/ui/AGENTS.md\`, and \`src/features/design-system/AGENTS.md\` before judging the implementation.
|
||
- Check every changed visual surface in both light and dark mode. Missing dark-mode support is a review issue, not visual polish.
|
||
- Review the selected changes and explain whether \`git diff --cached --name-only --some-extremely-long-inline-option-that-must-wrap\` stays inside the catalog detail column.
|
||
|
||
\`\`\`text
|
||
This deliberately long fenced-code example must not establish the minimum width of the full custom-agent instruction document or force earlier prose outside the catalog detail pane.
|
||
\`\`\`
|
||
|
||
| Before | After | Why |
|
||
| --- | --- | --- |
|
||
| \`transition: all 300ms\` | \`transition: transform 200ms ease-out\` | Specify exact properties so a wide instruction table stays independently scrollable without expanding the full catalog detail pane. |
|
||
| \`transform: scale(0)\` | \`transform: scale(0.95); opacity: 0\` | Preserve physicality while keeping the shared agent instructions inside their container. |`,
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.evaluate(() => {
|
||
document.documentElement.style.fontSize = "24px";
|
||
});
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await openPersonaCatalog(page);
|
||
await expect(
|
||
page.getByTestId(`persona-catalog-list-item-${personaId}`),
|
||
).toHaveCount(0);
|
||
await page.keyboard.press("Escape");
|
||
|
||
await page.getByLabel("Open actions for Catalog Analyst").click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
const catalogAccess = page.getByTestId("persona-share-catalog-access");
|
||
const shareDialog = page.getByTestId("persona-share-dialog");
|
||
const shareMainCard = shareDialog.getByTestId("persona-share-main-card");
|
||
const copyLinkButton = shareDialog.getByTestId("persona-share-copy-link");
|
||
const catalogSection = shareDialog.getByTestId("persona-share-catalog");
|
||
await expect(shareMainCard.getByTestId("persona-share-catalog")).toHaveCount(
|
||
0,
|
||
);
|
||
await expect(catalogSection).toContainText("Share to catalog");
|
||
await expect(catalogSection).toContainText(
|
||
"Anyone in this community can find and use a copy.",
|
||
);
|
||
await expect(catalogSection).toContainText(
|
||
"Your agent instruction is shared as plaintext. Memories and secrets aren’t included.",
|
||
);
|
||
const [copyLinkButtonBox, catalogSectionBox, shareMainCardBox] =
|
||
await Promise.all([
|
||
copyLinkButton.boundingBox(),
|
||
catalogSection.boundingBox(),
|
||
shareMainCard.boundingBox(),
|
||
]);
|
||
// Catalog is its own card below the sharing controls.
|
||
expect(
|
||
(copyLinkButtonBox?.y ?? 0) + (copyLinkButtonBox?.height ?? 0),
|
||
).toBeLessThanOrEqual(
|
||
(shareMainCardBox?.y ?? 0) + (shareMainCardBox?.height ?? 0),
|
||
);
|
||
expect(catalogSectionBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(shareMainCardBox?.y ?? 0) + (shareMainCardBox?.height ?? 0),
|
||
);
|
||
await expect(catalogAccess).not.toBeChecked();
|
||
await catalogAccess.click();
|
||
await expect(catalogAccess).toBeChecked();
|
||
const storedPersonas = await invokeTauri<
|
||
Array<{ id: string; shared: boolean }>
|
||
>(page, "list_personas");
|
||
expect(
|
||
storedPersonas.find((persona) => persona.id === personaId)?.shared,
|
||
).toBe(true);
|
||
await page
|
||
.getByTestId("persona-share-dialog")
|
||
.getByRole("button", { name: "Close" })
|
||
.click();
|
||
|
||
await openPersonaCatalog(page);
|
||
await expect(
|
||
page.getByTestId(`persona-catalog-list-item-${personaId}`),
|
||
).toContainText("Catalog Analyst");
|
||
await selectCatalogPersona(page, personaId);
|
||
const catalogDialog = page.getByTestId("persona-catalog-dialog");
|
||
const catalogDetailPane = page.getByTestId("persona-catalog-detail-pane");
|
||
await expect(catalogDetailPane).toContainText("Design System And Styling");
|
||
await expect(catalogDialog).toBeVisible();
|
||
await expect(catalogDetailPane).toBeVisible();
|
||
await waitForAnimations(page);
|
||
const [catalogDialogRight, catalogDetailPaneRight] = await Promise.all([
|
||
catalogDialog.evaluate((element) => element.getBoundingClientRect().right),
|
||
catalogDetailPane.evaluate(
|
||
(element) => element.getBoundingClientRect().right,
|
||
),
|
||
]);
|
||
expect(catalogDetailPaneRight).toBeLessThanOrEqual(catalogDialogRight);
|
||
expect(
|
||
await catalogDetailPane.evaluate(
|
||
(element) => element.scrollWidth - element.clientWidth,
|
||
),
|
||
).toBeLessThanOrEqual(1);
|
||
const catalogInstruction = catalogDetailPane.locator(".message-markdown");
|
||
expect(
|
||
await catalogInstruction.evaluate(
|
||
(element) => element.scrollWidth - element.clientWidth,
|
||
),
|
||
).toBeLessThanOrEqual(1);
|
||
await page.keyboard.press("Escape");
|
||
|
||
await page.getByLabel("Open actions for Catalog Analyst").click();
|
||
await page.getByRole("menuitem", { name: "Edit" }).click();
|
||
const editDialog = page.getByTestId("persona-dialog");
|
||
const catalogPublishNotice = editDialog.getByTestId(
|
||
"persona-dialog-catalog-publish-notice",
|
||
);
|
||
await expect(catalogPublishNotice).toHaveCount(0);
|
||
await expect(
|
||
editDialog.getByRole("button", { name: "Save and publish" }),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
editDialog.getByRole("button", { name: "Save changes" }),
|
||
).toBeVisible();
|
||
await editDialog
|
||
.getByLabel("Agent instructions")
|
||
.fill("Review the latest catalog changes.");
|
||
await expect(catalogPublishNotice).toHaveText(
|
||
"This agent is in the community catalog. Your changes will be published when you save.",
|
||
);
|
||
await expect(
|
||
editDialog.getByRole("button", { name: "Save changes" }),
|
||
).toHaveCount(0);
|
||
await editDialog.getByRole("button", { name: "Save and publish" }).click();
|
||
await expect(editDialog).toHaveCount(0);
|
||
// The promise in the button label is only kept by the command that awaits the
|
||
// relay; a plain `update_persona` merely enqueues a head best-effort.
|
||
await expect
|
||
.poll(() => countCommandInvocations(page, "update_persona_and_publish"))
|
||
.toBe(1);
|
||
expect(await countCommandInvocations(page, "update_persona")).toBe(0);
|
||
await expect(
|
||
page.getByText(
|
||
"Updated Catalog Analyst and published it to the community catalog.",
|
||
),
|
||
).toBeVisible();
|
||
|
||
await openPersonaCatalog(page);
|
||
await selectCatalogPersona(page, personaId);
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Review the latest catalog changes.",
|
||
);
|
||
await page.keyboard.press("Escape");
|
||
|
||
await page.getByLabel("Open actions for Catalog Analyst").click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
await expect(catalogAccess).toBeChecked();
|
||
await catalogAccess.click();
|
||
await expect(catalogAccess).not.toBeChecked();
|
||
await page
|
||
.getByTestId("persona-share-dialog")
|
||
.getByRole("button", { name: "Close" })
|
||
.click();
|
||
|
||
await openPersonaCatalog(page);
|
||
await expect(
|
||
page.getByTestId(`persona-catalog-list-item-${personaId}`),
|
||
).toHaveCount(0);
|
||
});
|
||
|
||
test("a queued catalog share is not presented as relay-published", async ({
|
||
page,
|
||
}) => {
|
||
const personaId = "custom:queued-catalog-agent";
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: personaId,
|
||
displayName: "Queued Catalog Agent",
|
||
systemPrompt: "Wait for relay acceptance.",
|
||
},
|
||
],
|
||
personaSharePublicationStatuses: ["queued"],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
await page.getByLabel("Open actions for Queued Catalog Agent").click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
await page.getByTestId("persona-share-catalog-access").click();
|
||
|
||
await expect(
|
||
page.getByText(
|
||
"Sharing Queued Catalog Agent is queued. It will appear after the relay accepts the update.",
|
||
),
|
||
).toBeVisible();
|
||
await page
|
||
.getByTestId("persona-share-dialog")
|
||
.getByRole("button", { name: "Close" })
|
||
.click();
|
||
|
||
await openPersonaCatalog(page);
|
||
await expect(
|
||
page.getByTestId(`persona-catalog-list-item-${personaId}`),
|
||
).toHaveCount(0);
|
||
});
|
||
|
||
test("a foreign reader does not receive an unshared kind 30175 persona", async ({
|
||
page,
|
||
}) => {
|
||
const personaId = "private-reviewer";
|
||
const remoteCatalogId = `catalog:${TEST_IDENTITIES.alice.pubkey}:${personaId}`;
|
||
await installMockBridge(page, {
|
||
personaCatalogEvents: [
|
||
createCatalogEvent({
|
||
ownerPubkey: TEST_IDENTITIES.alice.pubkey,
|
||
sourcePersonaId: personaId,
|
||
displayName: "Alice’s Private Reviewer",
|
||
systemPrompt: "This instruction must remain private.",
|
||
shared: false,
|
||
}),
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await openPersonaCatalog(page);
|
||
|
||
await expect(
|
||
page.getByTestId(`persona-catalog-list-item-${remoteCatalogId}`),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
page.getByText("No shared agents", { exact: true }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("a catalog entry keeps the owner's emoji avatar", async ({ page }) => {
|
||
const personaId = "emoji-reviewer";
|
||
const remoteCatalogId = `catalog:${TEST_IDENTITIES.alice.pubkey}:${personaId}`;
|
||
// Emoji avatars persist as inline percent-encoded SVG rather than a hosted
|
||
// URL, so build the value with the same producer the editor uses — a
|
||
// hand-rolled data URL would pass even if the real shape stopped matching.
|
||
const avatarUrl = emojiAvatarDataUrl("🐝", "#FFCC00");
|
||
await installMockBridge(page, {
|
||
personaCatalogEvents: [
|
||
createCatalogEvent({
|
||
avatarUrl,
|
||
ownerPubkey: TEST_IDENTITIES.alice.pubkey,
|
||
sourcePersonaId: personaId,
|
||
displayName: "Alice’s Reviewer",
|
||
systemPrompt: "Review changes for the whole community.",
|
||
}),
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await openPersonaCatalog(page);
|
||
|
||
// An `<img>` carrying the avatar — not the initials fallback — in both the
|
||
// list row and the detail header is what proves the projection kept it.
|
||
const remoteEntry = page.getByTestId(
|
||
`persona-catalog-list-item-${remoteCatalogId}`,
|
||
);
|
||
await expect(remoteEntry.locator("img")).toHaveAttribute("src", avatarUrl);
|
||
await remoteEntry.click();
|
||
await expect(
|
||
page.getByTestId("persona-catalog-detail-pane").locator("img").first(),
|
||
).toHaveAttribute("src", avatarUrl);
|
||
});
|
||
|
||
test("a community member can discover and add another member's catalog agent", async ({
|
||
page,
|
||
}) => {
|
||
const personaId = "shared-reviewer";
|
||
const remoteCatalogId = `catalog:${TEST_IDENTITIES.alice.pubkey}:${personaId}`;
|
||
await installMockBridge(page, {
|
||
personaCatalogEvents: [
|
||
createCatalogEvent({
|
||
ownerPubkey: TEST_IDENTITIES.alice.pubkey,
|
||
sourcePersonaId: personaId,
|
||
displayName: "Alice’s Reviewer",
|
||
systemPrompt: "Review changes for the whole community.",
|
||
}),
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await openPersonaCatalog(page);
|
||
|
||
const remoteEntry = page.getByTestId(
|
||
`persona-catalog-list-item-${remoteCatalogId}`,
|
||
);
|
||
await expect(remoteEntry).toContainText("Alice’s Reviewer");
|
||
await remoteEntry.click();
|
||
// The detail pane resolves the publisher's display name; 'Community member'
|
||
// is only the fallback for an unresolvable pubkey.
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Added by alice",
|
||
);
|
||
|
||
await page
|
||
.getByRole("button", {
|
||
name: "Add Alice’s Reviewer from Agent Catalog",
|
||
})
|
||
.click();
|
||
await expect
|
||
.poll(() => countCommandInvocations(page, "create_persona"))
|
||
.toBe(1);
|
||
const imported = await invokeTauri<
|
||
Array<{
|
||
display_name: string;
|
||
system_prompt: string;
|
||
shared: boolean;
|
||
catalog_source: { owner_pubkey: string; persona_id: string } | null;
|
||
}>
|
||
>(page, "list_personas");
|
||
expect(
|
||
imported.find((persona) => persona.display_name === "Alice’s Reviewer"),
|
||
).toMatchObject({
|
||
system_prompt: "Review changes for the whole community.",
|
||
shared: false,
|
||
// Provenance is what lets the catalog recognise the copy on the next open.
|
||
catalog_source: {
|
||
owner_pubkey: TEST_IDENTITIES.alice.pubkey,
|
||
persona_id: personaId,
|
||
},
|
||
});
|
||
|
||
// Reopening must offer the entry as already added rather than minting a
|
||
// second copy — the copy has a fresh local id, so only the stored
|
||
// coordinate can link it back to Alice's publication.
|
||
await page.keyboard.press("Escape");
|
||
await openPersonaCatalog(page);
|
||
// The entry now projects onto the local copy, so its list-item testid is the
|
||
// local persona id rather than the catalog coordinate.
|
||
await expect(
|
||
page.getByTestId(`persona-catalog-list-item-${remoteCatalogId}`),
|
||
).toHaveCount(0);
|
||
await page
|
||
.locator('[data-testid^="persona-catalog-list-item-"]')
|
||
.filter({ hasText: "Alice’s Reviewer" })
|
||
.click();
|
||
const addedTarget = page.getByRole("button", {
|
||
name: "Alice’s Reviewer is already in My Agents",
|
||
});
|
||
await expect(addedTarget).toBeDisabled();
|
||
await expect(addedTarget).toHaveText("Added to My Agents");
|
||
expect(await countCommandInvocations(page, "create_persona")).toBe(1);
|
||
});
|
||
|
||
test("catalog detail shows Community member when the publisher profile cannot be resolved", async ({
|
||
page,
|
||
}) => {
|
||
// A pubkey that is not in the mock profile registry — profile resolution
|
||
// will fail and the detail pane must fall back gracefully.
|
||
const unknownPubkey =
|
||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
|
||
const personaId = "unresolvable-reviewer";
|
||
await installMockBridge(page, {
|
||
personaCatalogEvents: [
|
||
createCatalogEvent({
|
||
ownerPubkey: unknownPubkey,
|
||
sourcePersonaId: personaId,
|
||
displayName: "Mystery Agent",
|
||
systemPrompt: "Published by someone whose profile cannot be fetched.",
|
||
}),
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
await openPersonaCatalog(page);
|
||
|
||
await page
|
||
.getByTestId(
|
||
`persona-catalog-list-item-catalog:${unknownPubkey}:${personaId}`,
|
||
)
|
||
.click();
|
||
await expect(page.getByTestId("persona-catalog-detail-pane")).toContainText(
|
||
"Added by Community member",
|
||
);
|
||
});
|
||
|
||
test("one share level selector drives both the link and send paths", async ({
|
||
page,
|
||
}) => {
|
||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||
const linkedAgentPubkey = TEST_IDENTITIES.alice.pubkey;
|
||
const profileAvatarUrl = "https://mock.relay/media/profile-only-avatar.png";
|
||
const profileAvatarBytes = Uint8Array.from(
|
||
atob(
|
||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=",
|
||
),
|
||
(character) => character.charCodeAt(0),
|
||
);
|
||
await page.route(profileAvatarUrl, async (route) => {
|
||
await route.fulfill({
|
||
body: Buffer.from(profileAvatarBytes),
|
||
contentType: "image/png",
|
||
status: 200,
|
||
});
|
||
});
|
||
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: "custom:animation-auditor",
|
||
displayName: "Animation Auditor",
|
||
systemPrompt: "You audit animations.",
|
||
},
|
||
],
|
||
managedAgents: [
|
||
{
|
||
pubkey: linkedAgentPubkey,
|
||
name: "Animation Auditor",
|
||
personaId: "custom:animation-auditor",
|
||
status: "running",
|
||
},
|
||
],
|
||
searchProfiles: [
|
||
{
|
||
pubkey: linkedAgentPubkey,
|
||
displayName: "Animation Auditor",
|
||
avatarUrl: profileAvatarUrl,
|
||
isAgent: true,
|
||
},
|
||
{
|
||
pubkey: TEST_IDENTITIES.charlie.pubkey,
|
||
displayName: "Charlie",
|
||
},
|
||
],
|
||
uploadDescriptors: [
|
||
{
|
||
url: `https://mock.relay/media/${"c".repeat(64)}.png`,
|
||
sha256: "c".repeat(64),
|
||
size: 1234,
|
||
type: "image/png",
|
||
uploaded: Math.floor(Date.now() / 1000),
|
||
filename: "animation-auditor.agent.png",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByLabel("Open actions for Animation Auditor").click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
|
||
const shareDialog = page.getByTestId("persona-share-dialog");
|
||
const shareMainCard = shareDialog.getByTestId("persona-share-main-card");
|
||
const initialShareCardHeight = await shareMainCard.evaluate(
|
||
(element) => element.getBoundingClientRect().height,
|
||
);
|
||
const shareLevel = shareDialog.getByLabel("What to include", {
|
||
exact: true,
|
||
});
|
||
const catalogAccess = shareDialog.getByLabel("Share to catalog");
|
||
const recipientField = page.getByTestId("persona-share-recipient-field");
|
||
const emptyRecipientFieldBox = await recipientField.boundingBox();
|
||
await expect(shareDialog.getByTestId("persona-share-send")).toHaveCount(0);
|
||
await expect(shareLevel).toHaveText("Agent only");
|
||
expect((await shareLevel.boundingBox())?.width).toBeLessThan(140);
|
||
expect(await shareLevel.evaluate((element) => element.tagName)).toBe(
|
||
"BUTTON",
|
||
);
|
||
await expect(shareLevel).toHaveCSS("text-decoration-line", "none");
|
||
await expect(shareLevel).toHaveCSS("padding-left", "8px");
|
||
await expect(shareLevel).toHaveCSS("padding-right", "8px");
|
||
await expect(catalogAccess).not.toBeChecked();
|
||
await expect(catalogAccess).toHaveCSS("cursor", "default");
|
||
await expect(
|
||
shareDialog.getByTestId("persona-share-link-settings"),
|
||
).toContainText("Share settings");
|
||
await expect(
|
||
shareDialog.getByText("Share settings", { exact: true }),
|
||
).toHaveClass(/text-xs.*text-secondary-foreground\/75/);
|
||
const copyLinkButton = shareDialog.getByTestId("persona-share-copy-link");
|
||
const recipientFieldBox = await recipientField.boundingBox();
|
||
const [shareLevelBox, copyLinkButtonBox, catalogAccessBox] =
|
||
await Promise.all([
|
||
shareLevel.boundingBox(),
|
||
copyLinkButton.boundingBox(),
|
||
catalogAccess.boundingBox(),
|
||
]);
|
||
// Reading order: who → what's included → copy link → catalog.
|
||
expect(shareLevelBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(recipientFieldBox?.y ?? 0) + (recipientFieldBox?.height ?? 0),
|
||
);
|
||
expect(copyLinkButtonBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(shareLevelBox?.y ?? 0) + (shareLevelBox?.height ?? 0),
|
||
);
|
||
expect(catalogAccessBox?.y ?? 0).toBeGreaterThanOrEqual(
|
||
(copyLinkButtonBox?.y ?? 0) + (copyLinkButtonBox?.height ?? 0),
|
||
);
|
||
// The memory choice is stated once, governing both delivery actions —
|
||
// neither the recipients row nor the link row carries its own copy.
|
||
await expect(
|
||
shareDialog.getByTestId("persona-share-recipient-access"),
|
||
).toHaveCount(0);
|
||
await expect(
|
||
shareDialog.getByTestId("persona-share-link-access"),
|
||
).toHaveCount(0);
|
||
await expect(shareLevel).toHaveCount(1);
|
||
await expect(
|
||
shareDialog.getByTestId("persona-share-memory-warning"),
|
||
).toHaveCount(0);
|
||
|
||
await shareLevel.click();
|
||
await expect(page.getByRole("menuitemradio")).toHaveText([
|
||
"Agent only",
|
||
"Agent + core memory",
|
||
"Agent + all memories",
|
||
]);
|
||
await page
|
||
.getByRole("menuitemradio", { name: "Agent + core memory" })
|
||
.click();
|
||
await expect(shareLevel).toHaveText("Agent + core memory");
|
||
await waitForAnimations(page);
|
||
const expandedShareCardHeight = await shareMainCard.evaluate(
|
||
(element) => element.getBoundingClientRect().height,
|
||
);
|
||
const inlineMemoryWarning = shareDialog.getByTestId(
|
||
"persona-share-memory-warning",
|
||
);
|
||
// No recipient is selected yet: the warning tracks the chosen contents, not
|
||
// whichever delivery button might be pressed.
|
||
await expect(shareDialog.getByTestId("persona-share-send")).toHaveCount(0);
|
||
await expect(inlineMemoryWarning).toBeVisible();
|
||
await expect(inlineMemoryWarning).toContainText(
|
||
"Memory is stored as plaintext in the snapshot.",
|
||
);
|
||
await expect(inlineMemoryWarning).toContainText(
|
||
"Only share it with people you trust.",
|
||
);
|
||
expect(expandedShareCardHeight).toBeGreaterThan(initialShareCardHeight);
|
||
await page.getByTestId("persona-share-copy-link").click();
|
||
const memoryConfirmation = page.getByTestId(
|
||
"persona-share-memory-confirmation",
|
||
);
|
||
await expect(memoryConfirmation).toBeVisible();
|
||
await expect(
|
||
memoryConfirmation.getByRole("heading", { name: "Share memories?" }),
|
||
).toBeVisible();
|
||
await expect(memoryConfirmation).toContainText("plaintext core memory");
|
||
await expect(memoryConfirmation).toContainText(
|
||
"Anyone with the link can view it.",
|
||
);
|
||
await expect(memoryConfirmation).toContainText(
|
||
"Only share with people you trust.",
|
||
);
|
||
const encodeLevelsBeforeLinkConfirmation = await page.evaluate(() =>
|
||
(
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
||
command: string;
|
||
payload: { memoryLevel?: string };
|
||
}>;
|
||
}
|
||
).__BUZZ_E2E_COMMAND_LOG__ ?? []
|
||
)
|
||
.filter((entry) => entry.command === "encode_agent_snapshot_for_send")
|
||
.map((entry) => entry.payload.memoryLevel),
|
||
);
|
||
expect(encodeLevelsBeforeLinkConfirmation).toEqual([]);
|
||
await memoryConfirmation.getByTestId("persona-share-memory-confirm").click();
|
||
await expect(page.getByTestId("persona-share-copy-link")).toContainText(
|
||
"Copied",
|
||
);
|
||
await shareLevel.click();
|
||
await page
|
||
.getByRole("menuitemradio", { name: "Agent only", exact: true })
|
||
.click();
|
||
await expect(shareLevel).toHaveText("Agent only");
|
||
await expect(inlineMemoryWarning).toHaveCount(0);
|
||
|
||
const recipientSearch = page.getByTestId("persona-share-recipient-search");
|
||
await recipientSearch.fill("charlie");
|
||
await page
|
||
.getByTestId(
|
||
`persona-share-recipient-option-${TEST_IDENTITIES.charlie.pubkey}`,
|
||
)
|
||
.click();
|
||
const recipientInputRegion = recipientField.getByTestId(
|
||
"persona-share-recipient-input-region",
|
||
);
|
||
await expect(recipientField).toHaveCSS("column-gap", "12px");
|
||
await expect(recipientInputRegion).toHaveCSS("flex-wrap", "wrap");
|
||
const sendButton = shareDialog.getByTestId("persona-share-send");
|
||
await expect(sendButton).toBeVisible();
|
||
await waitForAnimations(page);
|
||
const resizedRecipientFieldBox = await recipientField.boundingBox();
|
||
expect(resizedRecipientFieldBox?.width).toBeLessThan(
|
||
emptyRecipientFieldBox?.width ?? 0,
|
||
);
|
||
await expect
|
||
.poll(async () => {
|
||
const [sendButtonBox, currentCopyLinkButtonBox] = await Promise.all([
|
||
sendButton.boundingBox(),
|
||
copyLinkButton.boundingBox(),
|
||
]);
|
||
return Math.abs(
|
||
(sendButtonBox?.x ?? 0) +
|
||
(sendButtonBox?.width ?? 0) -
|
||
((currentCopyLinkButtonBox?.x ?? 0) +
|
||
(currentCopyLinkButtonBox?.width ?? 0)),
|
||
);
|
||
})
|
||
.toBeLessThanOrEqual(1);
|
||
// Same single selector now drives the send path; picking a level here is
|
||
// what the send confirmation must report.
|
||
await shareLevel.click();
|
||
await page
|
||
.getByRole("menuitemradio", { name: "Agent + all memories" })
|
||
.click();
|
||
await expect(shareLevel).toHaveText("Agent + all memories");
|
||
await expect(inlineMemoryWarning).toBeVisible();
|
||
await waitForAnimations(page);
|
||
expect(
|
||
await shareLevel
|
||
.locator("span")
|
||
.evaluate((element) => element.scrollWidth <= element.clientWidth),
|
||
).toBe(true);
|
||
await page.getByTestId("persona-share-send").click();
|
||
await expect(memoryConfirmation).toBeVisible();
|
||
await expect(memoryConfirmation).toContainText("plaintext all memories");
|
||
await expect(memoryConfirmation).toContainText(
|
||
"Charlie—and anyone with the file link—can view it.",
|
||
);
|
||
const encodeLevelsBeforeSendConfirmation = await page.evaluate(() =>
|
||
(
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
||
command: string;
|
||
payload: { memoryLevel?: string };
|
||
}>;
|
||
}
|
||
).__BUZZ_E2E_COMMAND_LOG__ ?? []
|
||
)
|
||
.filter((entry) => entry.command === "encode_agent_snapshot_for_send")
|
||
.map((entry) => entry.payload.memoryLevel),
|
||
);
|
||
expect(encodeLevelsBeforeSendConfirmation).not.toContain("none");
|
||
expect(encodeLevelsBeforeSendConfirmation).toContain("core");
|
||
expect(encodeLevelsBeforeSendConfirmation).not.toContain("everything");
|
||
await memoryConfirmation.getByTestId("persona-share-memory-confirm").click();
|
||
await expect(
|
||
page.getByText("Sent a copy of Animation Auditor"),
|
||
).toBeVisible();
|
||
|
||
const encodePayloads = await page.evaluate(() =>
|
||
(
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_COMMAND_LOG__?: Array<{
|
||
command: string;
|
||
payload: unknown;
|
||
}>;
|
||
}
|
||
).__BUZZ_E2E_COMMAND_LOG__ ?? []
|
||
)
|
||
.filter((entry) => entry.command === "encode_agent_snapshot_for_send")
|
||
.map((entry) => entry.payload),
|
||
);
|
||
expect(
|
||
encodePayloads.filter(
|
||
(payload) => (payload as { memoryLevel?: string }).memoryLevel === "none",
|
||
),
|
||
).toHaveLength(0);
|
||
expect(encodePayloads).toEqual(
|
||
expect.arrayContaining([
|
||
expect.objectContaining({
|
||
memoryLevel: "core",
|
||
memorySourcePubkey: linkedAgentPubkey,
|
||
avatarPngDataUrl: `data:image/png;base64,${Buffer.from(
|
||
profileAvatarBytes,
|
||
).toString("base64")}`,
|
||
}),
|
||
expect.objectContaining({
|
||
memoryLevel: "everything",
|
||
memorySourcePubkey: linkedAgentPubkey,
|
||
}),
|
||
]),
|
||
);
|
||
});
|
||
|
||
test("people sharing excludes the moderation recipient", async ({ page }) => {
|
||
await openSafetyShareDialog(page, {
|
||
relaySelf: TEST_IDENTITIES.charlie.pubkey,
|
||
relaySelfDelayMs: 800,
|
||
});
|
||
|
||
const search = page.getByTestId("persona-share-recipient-search");
|
||
await expect(search).toBeEnabled({ timeout: 5_000 });
|
||
await search.fill("charlie");
|
||
await expect(
|
||
page.getByTestId(
|
||
`persona-share-recipient-option-${TEST_IDENTITIES.charlie.pubkey}`,
|
||
),
|
||
).toHaveCount(0);
|
||
await expect(page.getByText("No people found.")).toBeVisible();
|
||
|
||
const commands = await readAgentShareCommands(page);
|
||
expect(
|
||
commands.filter((entry) =>
|
||
[
|
||
"open_dm",
|
||
"encode_agent_snapshot_for_send",
|
||
"upload_media_bytes",
|
||
"send_channel_message",
|
||
].includes(entry.command),
|
||
),
|
||
).toEqual([]);
|
||
});
|
||
|
||
test("people sharing blocks a timeout before encoding or upload", async ({
|
||
page,
|
||
}) => {
|
||
await openSafetyShareDialog(page);
|
||
await selectCharlieRecipient(page);
|
||
await page.evaluate(() => {
|
||
(
|
||
window as Window & {
|
||
__BUZZ_E2E_ACTIVATE_TIMEOUT__?: (expiresAtMs: number) => void;
|
||
}
|
||
).__BUZZ_E2E_ACTIVATE_TIMEOUT__?.(Date.now() + 60_000);
|
||
});
|
||
|
||
await page.getByTestId("persona-share-send").click();
|
||
await expect(page.getByText("Couldn’t send agent. Try again.")).toBeVisible();
|
||
|
||
const commands = await readAgentShareCommands(page);
|
||
expect(
|
||
commands.filter((entry) =>
|
||
[
|
||
"encode_agent_snapshot_for_send",
|
||
"upload_media_bytes",
|
||
"send_channel_message",
|
||
].includes(entry.command),
|
||
),
|
||
).toEqual([]);
|
||
});
|
||
|
||
test("people sharing rechecks destination eligibility after encoding", async ({
|
||
page,
|
||
}) => {
|
||
await openSafetyShareDialog(page, { encodeDelayMs: 800 });
|
||
await selectCharlieRecipient(page);
|
||
await page.getByTestId("persona-share-send").click();
|
||
|
||
await expect
|
||
.poll(async () => {
|
||
const commands = await readAgentShareCommands(page);
|
||
return commands.filter(
|
||
(entry) => entry.command === "encode_agent_snapshot_for_send",
|
||
).length;
|
||
})
|
||
.toBe(1);
|
||
|
||
await page.evaluate(() => {
|
||
const testWindow = window as Window & {
|
||
__BUZZ_E2E_MUTATE_CHANNEL__?: (options: {
|
||
channelId: string;
|
||
channelType: "forum";
|
||
}) => void;
|
||
__BUZZ_E2E_INVALIDATE_CHANNELS__?: () => Promise<void>;
|
||
};
|
||
testWindow.__BUZZ_E2E_MUTATE_CHANNEL__?.({
|
||
channelId: "d1ec7000-d000-4000-8000-000000000001",
|
||
channelType: "forum",
|
||
});
|
||
return testWindow.__BUZZ_E2E_INVALIDATE_CHANNELS__?.();
|
||
});
|
||
|
||
await expect(page.getByText("Couldn’t send agent. Try again.")).toBeVisible({
|
||
timeout: 5_000,
|
||
});
|
||
const commands = await readAgentShareCommands(page);
|
||
expect(
|
||
commands.filter(
|
||
(entry) => entry.command === "encode_agent_snapshot_for_send",
|
||
),
|
||
).toHaveLength(1);
|
||
expect(
|
||
commands.filter((entry) => entry.command === "upload_media_bytes"),
|
||
).toHaveLength(0);
|
||
expect(
|
||
commands.filter((entry) => entry.command === "send_channel_message"),
|
||
).toHaveLength(0);
|
||
});
|
||
|
||
test("people sharing guards the full action against duplicate sends", async ({
|
||
page,
|
||
}) => {
|
||
await openSafetyShareDialog(page, { encodeDelayMs: 500 });
|
||
await selectCharlieRecipient(page);
|
||
await page.getByTestId("persona-share-send").evaluate((button) => {
|
||
if (!(button instanceof HTMLButtonElement)) return;
|
||
button.click();
|
||
button.click();
|
||
});
|
||
|
||
await expect(page.getByText("Sent a copy of Safety Auditor")).toBeVisible({
|
||
timeout: 5_000,
|
||
});
|
||
await expect(page.getByText("Couldn’t send agent. Try again.")).toHaveCount(
|
||
0,
|
||
);
|
||
const commands = await readAgentShareCommands(page);
|
||
for (const command of [
|
||
"open_dm",
|
||
"encode_agent_snapshot_for_send",
|
||
"upload_media_bytes",
|
||
"send_channel_message",
|
||
]) {
|
||
expect(commands.filter((entry) => entry.command === command)).toHaveLength(
|
||
1,
|
||
);
|
||
}
|
||
});
|
||
|
||
test("people sharing stays mounted while a send is pending", async ({
|
||
page,
|
||
}) => {
|
||
await openSafetyShareDialog(page, { encodeDelayMs: 800 });
|
||
await selectCharlieRecipient(page);
|
||
await page.getByTestId("persona-share-send").click();
|
||
|
||
await page.keyboard.press("Escape");
|
||
await expect(page.getByTestId("persona-share-dialog")).toBeVisible();
|
||
await expect(page.getByText("Sent a copy of Safety Auditor")).toBeVisible({
|
||
timeout: 5_000,
|
||
});
|
||
});
|
||
|
||
test("export from share aligns selections and animates memory details", async ({
|
||
page,
|
||
}) => {
|
||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: "custom:animation-auditor",
|
||
displayName: "Animation Auditor",
|
||
systemPrompt: "You audit animations.",
|
||
},
|
||
],
|
||
managedAgents: [
|
||
{
|
||
pubkey: TEST_IDENTITIES.alice.pubkey,
|
||
name: "Animation Auditor",
|
||
personaId: "custom:animation-auditor",
|
||
status: "running",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByLabel("Open actions for Animation Auditor").click();
|
||
await page.getByRole("menuitem", { name: "Share" }).click();
|
||
await page.getByTestId("persona-share-export").click();
|
||
|
||
const exportDialog = page.getByTestId("agent-snapshot-export-dialog");
|
||
const memoryTrigger = exportDialog.getByLabel("Memories");
|
||
await expect(memoryTrigger).toHaveText("Agent only");
|
||
expect((await memoryTrigger.boundingBox())?.width).toBeLessThan(112);
|
||
await expect(memoryTrigger).toHaveCSS("text-align", "right");
|
||
await expect(memoryTrigger.locator("svg.lucide-chevron-down")).toBeVisible();
|
||
|
||
const initialHeight = await exportDialog.evaluate(
|
||
(element) => element.getBoundingClientRect().height,
|
||
);
|
||
await memoryTrigger.click();
|
||
await page
|
||
.getByRole("menuitemradio", { name: "Agent + core memory" })
|
||
.click();
|
||
const heightSamples = await exportDialog.evaluate(async (element) => {
|
||
const samples: number[] = [];
|
||
const start = performance.now();
|
||
|
||
await new Promise<void>((resolve) => {
|
||
const sample = (now: number) => {
|
||
samples.push(element.getBoundingClientRect().height);
|
||
if (now - start >= 280) {
|
||
resolve();
|
||
return;
|
||
}
|
||
requestAnimationFrame(sample);
|
||
};
|
||
requestAnimationFrame(sample);
|
||
});
|
||
|
||
return samples;
|
||
});
|
||
|
||
await expect(
|
||
exportDialog.getByTestId("agent-snapshot-memory-warning"),
|
||
).toBeVisible();
|
||
expect(heightSamples.at(-1)).toBeGreaterThan(initialHeight);
|
||
expect(
|
||
new Set(heightSamples.map((height) => Math.round(height))).size,
|
||
).toBeGreaterThan(2);
|
||
});
|
||
|
||
test("team-managed personas do not expose editable actions", async ({
|
||
page,
|
||
}) => {
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: "team:analyst",
|
||
displayName: "Team Analyst",
|
||
sourceTeam: "team-research-002",
|
||
systemPrompt: "You are Team Analyst.",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await page.getByLabel("Open actions for Team Analyst").click();
|
||
|
||
await expect(page.getByRole("menuitem")).toHaveText([
|
||
"Duplicate",
|
||
"Share",
|
||
"Managed by team",
|
||
]);
|
||
await expect(page.getByRole("menuitem", { name: "Edit" })).toHaveCount(0);
|
||
await expect(page.getByRole("menuitem", { name: "Delete" })).toHaveCount(0);
|
||
});
|
||
|
||
test("inactive built-ins cannot be used to create teams", async ({ page }) => {
|
||
await gotoApp(page);
|
||
|
||
const error = await invokeTauriExpectError(page, "create_team", {
|
||
input: {
|
||
name: "Honeys",
|
||
personaIds: ["builtin:honey"],
|
||
},
|
||
});
|
||
|
||
expect(error).toBe("Honey is not in My Agents.");
|
||
});
|
||
|
||
test("built-in removal failures show up from My Agents", async ({ page }) => {
|
||
await installMockBridge(page, {
|
||
activePersonaIds: ["builtin:honey"],
|
||
});
|
||
await gotoApp(page);
|
||
|
||
await page.getByTestId("open-agents-view").click();
|
||
await invokeTauri(page, "create_team", {
|
||
input: {
|
||
name: "Honeys",
|
||
personaIds: ["builtin:honey"],
|
||
},
|
||
});
|
||
|
||
await page.getByLabel("Open actions for Honey").click();
|
||
await page.getByRole("menuitem", { name: "Delete" }).click();
|
||
|
||
await expect(
|
||
page
|
||
.locator("[data-sonner-toast]")
|
||
.filter({ hasText: "Honey is still referenced by a team." }),
|
||
).toBeVisible();
|
||
});
|
||
|
||
test("personas referenced by teams cannot be deleted", async ({ page }) => {
|
||
await gotoApp(page);
|
||
|
||
const persona = await invokeTauri<{ id: string }>(page, "create_persona", {
|
||
input: {
|
||
displayName: "Analyst",
|
||
systemPrompt: "You are Analyst.",
|
||
},
|
||
});
|
||
|
||
await invokeTauri(page, "create_team", {
|
||
input: {
|
||
name: "Analysts",
|
||
personaIds: [persona.id],
|
||
},
|
||
});
|
||
|
||
const error = await invokeTauriExpectError(page, "delete_persona", {
|
||
id: persona.id,
|
||
});
|
||
|
||
expect(error).toBe(
|
||
"Analyst is still referenced by a team. Remove it from those teams first.",
|
||
);
|
||
});
|
||
|
||
test("start pill morphs into the running dot without remounting the avatar", async ({
|
||
page,
|
||
}) => {
|
||
const personaId = "custom:motion-auditor";
|
||
const pubkey = "ab".repeat(32);
|
||
const activeDotSize = 18;
|
||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
avatarUrl: emojiAvatarDataUrl("✨", "#7657FF"),
|
||
displayName: "Motion Auditor",
|
||
id: personaId,
|
||
systemPrompt: "You audit motion continuity.",
|
||
},
|
||
],
|
||
managedAgents: [
|
||
{
|
||
name: "Motion Auditor",
|
||
personaId,
|
||
pubkey,
|
||
status: "stopped",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
const card = page.getByTestId(`persona-agent-row-${personaId}`);
|
||
const startButton = page.getByTestId(`agent-runtime-start-${pubkey}`);
|
||
const badge = startButton.locator("xpath=../..");
|
||
const initialAvatar = await card
|
||
.getByAltText("Motion Auditor avatar")
|
||
.elementHandle();
|
||
expect(initialAvatar).not.toBeNull();
|
||
|
||
const samplesPromise = badge.evaluate(async (element) => {
|
||
const samples: Array<{
|
||
backgroundColor: string;
|
||
height: number;
|
||
width: number;
|
||
}> = [];
|
||
const startedAt = performance.now();
|
||
|
||
while (performance.now() - startedAt < 440) {
|
||
const bounds = element.getBoundingClientRect();
|
||
samples.push({
|
||
backgroundColor: getComputedStyle(element).backgroundColor,
|
||
height: bounds.height,
|
||
width: bounds.width,
|
||
});
|
||
await new Promise<void>((resolve) =>
|
||
requestAnimationFrame(() => resolve()),
|
||
);
|
||
}
|
||
|
||
return samples;
|
||
});
|
||
|
||
await page.waitForTimeout(32);
|
||
await startButton.click();
|
||
await expect(
|
||
page.getByTestId(`agent-runtime-active-${pubkey}`),
|
||
).toBeVisible();
|
||
const samples = await samplesPromise;
|
||
const finalAvatar = await card
|
||
.getByAltText("Motion Auditor avatar")
|
||
.elementHandle();
|
||
|
||
expect(samples[0]?.width).toBeCloseTo(56, 0);
|
||
expect(samples[0]?.height).toBeCloseTo(36, 0);
|
||
expect(
|
||
samples.some(
|
||
(sample) =>
|
||
sample.width > activeDotSize &&
|
||
sample.width < 56 &&
|
||
sample.height > activeDotSize &&
|
||
sample.height < 36,
|
||
),
|
||
).toBe(true);
|
||
expect(samples.at(-1)?.width).toBeCloseTo(activeDotSize, 0);
|
||
expect(samples.at(-1)?.height).toBeCloseTo(activeDotSize, 0);
|
||
expect(samples.at(-1)?.backgroundColor).not.toBe(samples[0]?.backgroundColor);
|
||
await expect(
|
||
page.getByTestId(`agent-runtime-active-${pubkey}`).locator("xpath=../.."),
|
||
).toHaveClass(/bg-emerald-500/);
|
||
expect(
|
||
await initialAvatar?.evaluate(
|
||
(before, after) => before === after,
|
||
finalAvatar,
|
||
),
|
||
).toBe(true);
|
||
});
|
||
|
||
test("duplicate instances move from the agents gallery into the agent profile", async ({
|
||
page,
|
||
}) => {
|
||
const personaId = "custom:duplicate-auditor";
|
||
const primaryPubkey = TEST_IDENTITIES.alice.pubkey;
|
||
const additionalPubkey = TEST_IDENTITIES.charlie.pubkey;
|
||
await installMockBridge(page, {
|
||
personas: [
|
||
{
|
||
id: personaId,
|
||
displayName: "Duplicate Auditor",
|
||
systemPrompt: "You audit duplicate instances.",
|
||
},
|
||
],
|
||
managedAgents: [
|
||
{
|
||
pubkey: primaryPubkey,
|
||
name: "Duplicate Auditor",
|
||
personaId,
|
||
status: "running",
|
||
},
|
||
{
|
||
pubkey: additionalPubkey,
|
||
name: "Duplicate Auditor",
|
||
personaId,
|
||
status: "stopped",
|
||
},
|
||
],
|
||
});
|
||
await gotoApp(page);
|
||
await page.getByTestId("open-agents-view").click();
|
||
|
||
await expect(page.getByText("Additional running agents")).toHaveCount(0);
|
||
await expect(
|
||
page.getByTestId(`managed-agent-${additionalPubkey}`),
|
||
).toHaveCount(0);
|
||
|
||
await page.getByTestId(`persona-agent-row-${personaId}`).click();
|
||
await page.getByTestId("user-profile-instances").click();
|
||
await page.getByTestId(`user-profile-instance-${additionalPubkey}`).click();
|
||
|
||
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
|
||
await page.getByTestId("user-profile-settings-menu-trigger").click();
|
||
await expect(
|
||
page.getByTestId(`user-profile-agent-delete-${additionalPubkey}`),
|
||
).toBeVisible();
|
||
});
|