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>
225 lines
7.0 KiB
TypeScript
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);
|
|
});
|