feat: import Chinese-localized Buzz source snapshot
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
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>
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
Reference in New Issue
Block a user