Files
buzz/desktop/tests/e2e/persona-sync.spec.ts
T
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

225 lines
7.0 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { hexToBytes } from "@noble/hashes/utils.js";
import { finalizeEvent } from "nostr-tools/pure";
import { installMockBridge } from "../helpers/bridge";
// Tyler's test identity — from TEST_IDENTITIES.tyler in tests/helpers/bridge.ts
const TYLER_PRIVATE_KEY = hexToBytes(
"3dbaebadb5dfd777ff25149ee230d907a15a9e1294b40b830661e65bb42f6c03",
);
const TYLER_PUBKEY =
"e5ebc6cdb579be112e336cc319b5989b4bb6af11786ea90dbe52b5f08d741b34";
const D_TAG = "sync-test-persona";
const KIND_PERSONA = 30175;
const KIND_DELETION = 5;
// The command scopes an inbound event to the community it arrived on. Under the
// mock bridge the app subscribes on e2eBridge's DEFAULT_RELAY_WS_URL, so that is
// the arrival relay these direct invocations stand in for.
const ARRIVAL_RELAY_URL = "ws://localhost:3000";
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
async function gotoApp(page: import("@playwright/test").Page) {
await page.goto("/", { waitUntil: "domcontentloaded" });
await waitForInvokeBridge(page);
await expect(page.getByTestId("open-agents-view")).toBeVisible({
timeout: 10_000,
});
}
async function waitForInvokeBridge(page: import("@playwright/test").Page) {
await page.waitForFunction(
() => {
const w = window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: unknown;
__TAURI_INTERNALS__?: { invoke?: unknown };
};
return (
typeof w.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ === "function" ||
typeof w.__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: c, payload: p }) => {
const w = window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
c: string,
p?: Record<string, unknown>,
) => Promise<unknown>;
__TAURI_INTERNALS__?: {
invoke?: (c: string, p?: Record<string, unknown>) => Promise<unknown>;
};
};
const invoke =
w.__BUZZ_E2E_INVOKE_MOCK_COMMAND__ ?? w.__TAURI_INTERNALS__?.invoke;
if (!invoke) throw new Error("Mock invoke bridge is unavailable.");
return (await invoke(c, p)) as T;
},
{ command, payload },
);
}
/**
* Register a one-shot `agents-data-changed` listener BEFORE the reconcile call
* and wait up to 500 ms for it to fire (covers the 200 ms debounce coalesce).
* Returns true if the event fired, false on timeout.
*
* Must be called before the invokeTauri that triggers the emit so the listener
* is registered before the event fires — no race.
*/
async function listenForAgentsDataChanged(
page: import("@playwright/test").Page,
): Promise<() => Promise<boolean>> {
// Inject a Promise into the page that resolves when the event fires.
await page.evaluate(() => {
(
window as Window & { __agentsDataChangedFired?: Promise<boolean> }
).__agentsDataChangedFired = new Promise<boolean>((resolve) => {
const internals = (
window as Window & {
__TAURI_INTERNALS__?: {
listen?: (event: string, cb: () => void) => Promise<() => void>;
};
}
).__TAURI_INTERNALS__;
if (!internals?.listen) {
resolve(false);
return;
}
void internals.listen("agents-data-changed", () => resolve(true));
// Timeout guard: 500 ms covers the 200 ms debounce coalesce with margin.
setTimeout(() => resolve(false), 500);
});
});
// Return a thunk the caller invokes after the reconcile to await the result.
return () =>
page.evaluate(
() =>
(window as Window & { __agentsDataChangedFired?: Promise<boolean> })
.__agentsDataChangedFired ?? Promise.resolve(false),
);
}
test("upsert round-trip: reconcile_inbound_persona_event writes record and emits agents-data-changed", async ({
page,
}) => {
await gotoApp(page);
const createdAt = Math.floor(Date.now() / 1000);
// Build + sign the kind:30175 persona event using nostr-tools.
const personaEvent = finalizeEvent(
{
kind: KIND_PERSONA,
content: JSON.stringify({
display_name: "Sync Test Persona",
system_prompt: "You are a sync test.",
}),
tags: [["d", D_TAG]],
created_at: createdAt,
},
TYLER_PRIVATE_KEY,
);
// Register the listener BEFORE the reconcile call — no race.
const awaitFired = await listenForAgentsDataChanged(page);
// Drive the inbound reconcile path.
await invokeTauri(page, "reconcile_inbound_persona_event", {
eventJson: JSON.stringify(personaEvent),
arrivalRelayUrl: ARRIVAL_RELAY_URL,
});
// Assert the record landed on disk.
const personas = await invokeTauri<
Array<{ id: string; display_name: string }>
>(page, "list_personas");
const record = personas.find((p) => p.id === D_TAG);
expect(record?.display_name).toBe("Sync Test Persona");
// Assert the emit fired (debounce settles within 500 ms timeout guard).
const fired = await awaitFired();
expect(fired).toBe(true);
});
test("tombstone round-trip: reconcile_inbound_persona_event removes record and emits agents-data-changed", async ({
page,
}) => {
await gotoApp(page);
const upsertCreatedAt = Math.floor(Date.now() / 1000);
// Step 1: upsert the persona first.
const personaEvent = finalizeEvent(
{
kind: KIND_PERSONA,
content: JSON.stringify({
display_name: "Sync Test Persona",
system_prompt: "You are a sync test.",
}),
tags: [["d", D_TAG]],
created_at: upsertCreatedAt,
},
TYLER_PRIVATE_KEY,
);
await invokeTauri(page, "reconcile_inbound_persona_event", {
eventJson: JSON.stringify(personaEvent),
arrivalRelayUrl: ARRIVAL_RELAY_URL,
});
// Step 2: confirm it landed.
const afterUpsert = await invokeTauri<Array<{ id: string }>>(
page,
"list_personas",
);
expect(afterUpsert.some((p) => p.id === D_TAG)).toBe(true);
// Step 3: build + sign a kind:5 deletion event. created_at must be strictly
// after the upsert so the retention db does not skip it.
const tombstoneEvent = finalizeEvent(
{
kind: KIND_DELETION,
content: "",
tags: [["a", `${KIND_PERSONA}:${TYLER_PUBKEY}:${D_TAG}`]],
created_at: upsertCreatedAt + 1,
},
TYLER_PRIVATE_KEY,
);
// Register the listener BEFORE the tombstone reconcile call — no race.
const awaitFired = await listenForAgentsDataChanged(page);
await invokeTauri(page, "reconcile_inbound_persona_event", {
eventJson: JSON.stringify(tombstoneEvent),
arrivalRelayUrl: ARRIVAL_RELAY_URL,
});
// Step 4: assert the record is gone.
const afterTombstone = await invokeTauri<Array<{ id: string }>>(
page,
"list_personas",
);
expect(afterTombstone.some((p) => p.id === D_TAG)).toBe(false);
// Step 5: assert the emit fired for the tombstone path too.
const fired = await awaitFired();
expect(fired).toBe(true);
});