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

358 lines
12 KiB
TypeScript

/**
* Recipient-native agent snapshot card/import E2E spec.
*
* These tests exercise the AgentSnapshotCard rendered in a message timeline
* when an .agent.json or .agent.png attachment is detected, and the full
* Add agent → preview → confirm flow.
*/
import { expect, test } from "@playwright/test";
import { installMockBridge } from "../helpers/bridge";
type CommandLogEntry = { command: string; payload: unknown };
function hasVisibleBoxShadow(boxShadow: string) {
if (boxShadow === "none") return false;
const colors = [...boxShadow.matchAll(/rgba?\(([^)]+)\)/gu)];
if (colors.length === 0) return true;
return colors.some(([, channels]) => {
const values = channels.split(",").map((value) => value.trim());
return values.length < 4 || Number(values[3]) > 0;
});
}
async function readCommandLog(page: import("@playwright/test").Page) {
return page.evaluate(
() =>
(
window as Window & {
__BUZZ_E2E_COMMAND_LOG__?: CommandLogEntry[];
}
).__BUZZ_E2E_COMMAND_LOG__ ?? [],
);
}
async function invokeMockCommand(
page: import("@playwright/test").Page,
command: string,
payload: Record<string, unknown>,
) {
// The app installs its dynamically imported bridge during bootstrap. Wait for
// that installation after navigation instead of racing the first invoke.
await page.waitForFunction(
() =>
typeof (
window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
}
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__ === "function",
null,
{ timeout: 5_000 },
);
return page.evaluate(
async ({ command: cmd, payload: request }) => {
const invoke = (
window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__: (
command: string,
payload: Record<string, unknown>,
) => Promise<unknown>;
}
).__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
return invoke(cmd, request);
},
{ command, payload },
);
}
const ANALYST_PERSONA_ID = "test-analyst";
const ANALYST_PUBKEY =
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f";
const SHA256 = "a".repeat(64);
/** Full imeta descriptor for a .agent.json attachment. */
const SNAPSHOT_UPLOAD_DESCRIPTOR = {
url: `https://mock.relay/media/${SHA256}.json`,
sha256: SHA256,
size: 1234,
type: "application/json",
uploaded: Math.floor(Date.now() / 1000),
filename: "e2e-agent.agent.json",
};
// ── Helper: seed bridge, send snapshot to #general, navigate to timeline ─────
async function seedAndSendSnapshot(
page: import("@playwright/test").Page,
opts: { snapshotFetchError?: string } = {},
) {
await installMockBridge(page, {
personas: [
{
id: ANALYST_PERSONA_ID,
displayName: "Analyst",
systemPrompt: "You are an analyst.",
},
],
managedAgents: [
{
pubkey: ANALYST_PUBKEY,
name: "Analyst",
personaId: ANALYST_PERSONA_ID,
},
],
uploadDescriptors: [SNAPSHOT_UPLOAD_DESCRIPTOR],
...(opts.snapshotFetchError
? { snapshotFetchError: opts.snapshotFetchError }
: {}),
});
await page.goto("/");
// JSON snapshots can still arrive from saved files or older clients, even
// though new in-app shares always encode PNG. Seed one directly so this
// recipient suite retains JSON-card coverage independent of send behavior.
await invokeMockCommand(page, "send_channel_message", {
channelId: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50",
content: `[${SNAPSHOT_UPLOAD_DESCRIPTOR.filename}](${SNAPSHOT_UPLOAD_DESCRIPTOR.url})`,
mediaTags: [
[
"imeta",
`url ${SNAPSHOT_UPLOAD_DESCRIPTOR.url}`,
`m ${SNAPSHOT_UPLOAD_DESCRIPTOR.type}`,
`x ${SNAPSHOT_UPLOAD_DESCRIPTOR.sha256}`,
`size ${SNAPSHOT_UPLOAD_DESCRIPTOR.size}`,
`filename ${SNAPSHOT_UPLOAD_DESCRIPTOR.filename}`,
],
],
});
// Navigate to #general.
await page.getByTestId("channel-general").click();
}
// ── Timeline renders AgentSnapshotCard, not generic FileCard ──────────────────
test("recipient_timeline_renders_agent_snapshot_card_not_file_card", async ({
page,
}) => {
await seedAndSendSnapshot(page);
// The sent attachment must render as AgentSnapshotCard.
const card = page.getByTestId("agent-snapshot-card").last();
await expect(card).toBeVisible({ timeout: 5000 });
await expect(card).toHaveAttribute("data-slot", "attachment");
// Show the agent name without the snapshot file extension.
const title = card.locator('[data-slot="attachment-title"]');
await expect(title).toBeVisible();
await expect(title).toHaveText("E2e Agent");
await expect(card).not.toContainText("e2e-agent.agent.json");
const mediaBox = await card
.locator('[data-slot="attachment-media"]')
.boundingBox();
expect(mediaBox?.width).toBe(mediaBox?.height);
// Metadata names the sharer and keeps the file size.
await expect(card).toContainText("Shared by npub1mock... · 1.2 KB");
await expect(card).not.toContainText("Agent snapshot");
// Both actions must be present.
await expect(card.getByTestId("agent-snapshot-card-import")).toBeVisible();
await expect(card.getByTestId("agent-snapshot-card-download")).toBeVisible();
await expect(card.locator('[data-slot="attachment-action"]')).toHaveCount(2);
const actions = card.locator('[data-slot="attachment-actions"] button');
await expect(actions.nth(0)).toHaveAccessibleName("Download E2e Agent");
await expect(actions.nth(0)).toHaveText("");
await expect(actions.nth(1)).toHaveText("Add agent");
const addAgentButton = actions.nth(1);
const restingButtonStyle = await addAgentButton.evaluate((element) => {
const style = getComputedStyle(element);
return {
backgroundColor: style.backgroundColor,
boxShadow: style.boxShadow,
color: style.color,
};
});
expect(restingButtonStyle.color).not.toBe(restingButtonStyle.backgroundColor);
expect(hasVisibleBoxShadow(restingButtonStyle.boxShadow)).toBe(false);
await addAgentButton.hover();
await expect
.poll(async () =>
hasVisibleBoxShadow(
await addAgentButton.evaluate(
(element) => getComputedStyle(element).boxShadow,
),
),
)
.toBe(false);
const [contentBox, downloadBox, addAgentBox] = await Promise.all([
card.locator('[data-slot="attachment-content"]').boundingBox(),
actions.nth(0).boundingBox(),
addAgentButton.boundingBox(),
]);
expect(
(downloadBox?.x ?? 0) - ((contentBox?.x ?? 0) + (contentBox?.width ?? 0)),
).toBeGreaterThanOrEqual(28);
expect(
(addAgentBox?.x ?? 0) - ((downloadBox?.x ?? 0) + (downloadBox?.width ?? 0)),
).toBeGreaterThanOrEqual(8);
const [titleElement, descriptionElement] = await Promise.all([
title.elementHandle(),
card.locator('[data-slot="attachment-description"]').elementHandle(),
]);
expect(
await titleElement?.evaluate(
(element) => element.scrollWidth <= element.clientWidth,
),
).toBe(true);
expect(
await descriptionElement?.evaluate(
(element) => element.scrollWidth <= element.clientWidth,
),
).toBe(true);
// Generic FileCard must NOT be present for this attachment.
await expect(page.getByTestId("file-card")).toHaveCount(0);
});
// ── Download still invokes download_file, not fetch_snapshot_bytes ───────────
test("recipient_download_invokes_download_file_only", async ({ page }) => {
await seedAndSendSnapshot(page);
const card = page.getByTestId("agent-snapshot-card").last();
await expect(card).toBeVisible({ timeout: 5000 });
await card.getByTestId("agent-snapshot-card-download").click();
const log = await readCommandLog(page);
const downloadCmds = log.filter((e) => e.command === "download_file");
const fetchSnapshotCmds = log.filter(
(e) => e.command === "fetch_snapshot_bytes",
);
expect(downloadCmds.length).toBeGreaterThanOrEqual(1);
expect(fetchSnapshotCmds.length).toBe(0);
});
// ── Add agent: fetch → navigate to agents → open preview ─────────────────────
test("recipient_import_navigates_to_agents_and_opens_preview", async ({
page,
}) => {
await seedAndSendSnapshot(page);
const card = page.getByTestId("agent-snapshot-card").last();
await expect(card).toBeVisible({ timeout: 5000 });
// Click Add agent.
await card.getByTestId("agent-snapshot-card-import").click();
// fetch_snapshot_bytes must have been called.
await expect(async () => {
const log = await readCommandLog(page);
const fetchCmds = log.filter((e) => e.command === "fetch_snapshot_bytes");
expect(fetchCmds.length).toBeGreaterThanOrEqual(1);
}).toPass({ timeout: 5000 });
// download_file must NOT have been called for Add agent.
const log = await readCommandLog(page);
const downloadCmds = log.filter((e) => e.command === "download_file");
expect(downloadCmds.length).toBe(0);
// Preview dialog must open on the agents view.
const dialog = page.getByTestId("agent-snapshot-import-dialog");
await expect(dialog).toBeVisible({ timeout: 8000 });
// Decoded display name must appear.
await expect(dialog).toContainText("Imported Agent");
const metadata = dialog.getByTestId("agent-definition-metadata");
await expect(metadata).toContainText("Type");
await expect(metadata).toContainText("Built-in agent");
await expect(metadata).toContainText("Preferred model");
await expect(metadata).toContainText("claude-opus-4-5");
await expect(metadata).toContainText("Preferred runtime");
await expect(metadata).toContainText("goose");
});
// ── Confirm imports the agent ─────────────────────────────────────────────────
test("recipient_import_confirm_calls_confirm_once_and_shows_result", async ({
page,
}) => {
await seedAndSendSnapshot(page);
const card = page.getByTestId("agent-snapshot-card").last();
await expect(card).toBeVisible({ timeout: 5000 });
await card.getByTestId("agent-snapshot-card-import").click();
const dialog = page.getByTestId("agent-snapshot-import-dialog");
await expect(dialog).toBeVisible({ timeout: 8000 });
// Confirm the import.
await dialog.getByTestId("agent-snapshot-import-confirm").click();
// confirm_agent_snapshot_import must have been called exactly once.
await expect(async () => {
const log = await readCommandLog(page);
const confirmCmds = log.filter(
(e) => e.command === "confirm_agent_snapshot_import",
);
expect(confirmCmds.length).toBe(1);
}).toPass({ timeout: 5000 });
});
// ── Malformed/error candidate shows error, never opens preview ────────────────
test("recipient_fetch_error_shows_error_and_download_remains", async ({
page,
}) => {
await seedAndSendSnapshot(page, {
snapshotFetchError: "hash mismatch: fetched bytes do not match",
});
const card = page.getByTestId("agent-snapshot-card").last();
await expect(card).toBeVisible({ timeout: 5000 });
await card.getByTestId("agent-snapshot-card-import").click();
// Error must appear; preview must NOT open.
const errorEl = card.getByTestId("agent-snapshot-card-error");
await expect(errorEl).toBeVisible({ timeout: 5000 });
await expect(errorEl).toContainText("hash mismatch");
await expect(
page.getByTestId("agent-snapshot-import-dialog"),
).not.toBeVisible();
// Download must remain available after error.
await expect(card.getByTestId("agent-snapshot-card-download")).toBeVisible();
});
// ── Double-click produces one fetch/preview flow ──────────────────────────────
test("recipient_double_click_import_opens_one_preview", async ({ page }) => {
await seedAndSendSnapshot(page);
const card = page.getByTestId("agent-snapshot-card").last();
await expect(card).toBeVisible({ timeout: 5000 });
const importBtn = card.getByTestId("agent-snapshot-card-import");
// Click Add agent.
await importBtn.click();
// Import dialog must open exactly once (not duplicated by rapid clicks).
const dialog = page.getByTestId("agent-snapshot-import-dialog");
await expect(dialog).toHaveCount(1, { timeout: 5000 });
// Exactly one fetch_snapshot_bytes call.
const log = await readCommandLog(page);
const fetchCount = log.filter(
(e) => e.command === "fetch_snapshot_bytes",
).length;
expect(fetchCount).toBe(1);
});