mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
## Summary - add `just desktop-release-smoke`, a deterministic desktop correctness/reachability smoke against an ephemeral real local relay - preserve existing DM history when the first live DM enters a pageless query window, the desktop-v0.5.10 disappearing-DM regression - enforce foreground JS ordering: a frame and actionable sidebar input must dispatch before mounted stale queries begin resume refetches, while separately requiring the navigation to commit promptly - seed a 10,000-event dense-second fixture and verify exact event-ID reachability, SHA-256 identity, ordering, duplicate absence, bounded mounted rows, and drained render work - isolate Postgres per run, serialize the shared Redis DB, retain phase/relay/Playwright diagnostics, and gate desktop release manifest assembly on the smoke This is deliberately **not a performance-regression gate**. CDP and action timing fields are informational only. There is no candidate/baseline comparison or threshold. A future performance lane needs repeated equivalent fixtures, discrete interaction samples, and an explicit comparator/noise policy. The diagnostics record the fixture version, row count, wall-clock base timestamp (`fixtureSecond`), expected event-ID hash, observed state, and measurements. Because the created-at floor requires a current timestamp, paired comparison remains disabled. The release job runs on an isolated GitHub-hosted runner. The script also guards automatic local runs with a Redis allocation lock. Its remaining direct-PID cleanup and free-port selection race mean it should not be repurposed onto a persistent concurrent shared runner without first hardening process-group cleanup and port reservation. ### Related issue N/A ### Testing - `pnpm --dir desktop typecheck` - focused real-local-relay release smoke passed after adversarial review fixes - identical DM witness passed current and failed `desktop-v0.5.10` with the history-loss signature - identical foreground witness bytes (`2c1e97df04c9b8ca0304b66bbbe9bdb4d08924ad8ce0f68a9c490458fcc3aca8`) failed `desktop-v0.5.10` structurally: the first resume fetch was marker 1, before first frame/sidebar dispatch at marker 8 - with PR #5696 (`59f613c40`) merged, the witness showed focus at 951.3 ms, first frame at 951.6 ms, click dispatch at 952.1 ms, first resume fetch at 968.9 ms, and route commit at 992.4 ms - the gate therefore protects first paint and actionable input dispatch; route commit is a bounded responsiveness witness, not a prerequisite for resume work - the corrected focused foreground scenario passed at `6d9b5be40da58bbee92a856b04c3558946d0a950`; the prior merged-tree full run passed DM retention and 10k reachability before exposing this contract mismatch - pre-push passed on exact pushed head `6d9b5be40da58bbee92a856b04c3558946d0a950`, including desktop checks, typecheck, desktop tests, Rust tests, mobile tests, and Tauri checks - full 10,000-event scenario reached 10,000/10,000 exact IDs with matching SHA-256, 199 continuation requests, and 95 mounted rows in about 4.4 minutes - reduced-row review run passed in 18.4 seconds ### Foreground witness boundary The Chromium test is a deterministic JS policy gate. Headless Chromium does not expose an honest blur/focus transition in this fixture, so the test drives the production focus listener and `document.hasFocus()` predicate together and records that simulation explicitly. It proves refetch fan-out ordering, not AppKit activation, WKWebView paint, or an activating physical click. A packaged macOS native lane is still required before claiming the actual desktop activation experience is certified. --------- Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
204 lines
6.2 KiB
TypeScript
204 lines
6.2 KiB
TypeScript
import { expect, test, type Page, type Route } from "@playwright/test";
|
|
import { hexToBytes } from "@noble/hashes/utils.js";
|
|
import { finalizeEvent, type VerifiedEvent } from "nostr-tools/pure";
|
|
|
|
import { installRelayBridge, TEST_IDENTITIES } from "../helpers/bridge";
|
|
import { assertRelaySeeded } from "../helpers/seed";
|
|
|
|
const RELAY_HTTP = process.env.BUZZ_E2E_RELAY_URL ?? "http://localhost:3000";
|
|
const DM_ID = "5a9c064e-0411-5242-ae6b-0363ba99b8e6";
|
|
|
|
async function publishAliceDm(
|
|
content: string,
|
|
createdAt: number,
|
|
): Promise<VerifiedEvent> {
|
|
const event = finalizeEvent(
|
|
{
|
|
kind: 9,
|
|
content,
|
|
created_at: createdAt,
|
|
tags: [
|
|
["h", DM_ID],
|
|
["p", TEST_IDENTITIES.tyler.pubkey],
|
|
],
|
|
},
|
|
hexToBytes(TEST_IDENTITIES.alice.privateKey),
|
|
);
|
|
const response = await fetch(`${RELAY_HTTP}/events`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json", "X-Pubkey": event.pubkey },
|
|
body: JSON.stringify(event),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`POST /events failed (${response.status}): ${await response.text()}`,
|
|
);
|
|
}
|
|
return event;
|
|
}
|
|
|
|
async function timelineIds(page: Page) {
|
|
return page.getByTestId("message-timeline").evaluate((element) =>
|
|
Array.from(element.querySelectorAll<HTMLElement>("[data-message-id]"))
|
|
.map((row) => row.dataset.messageId)
|
|
.filter((id): id is string => Boolean(id)),
|
|
);
|
|
}
|
|
|
|
async function cachedMessageIds(page: Page) {
|
|
return page.evaluate((channelId) => {
|
|
const client = window.__BUZZ_E2E_QUERY_CLIENT__ as unknown as {
|
|
getQueryData: (
|
|
key: readonly unknown[],
|
|
) => Array<{ id: string }> | undefined;
|
|
};
|
|
if (!client) throw new Error("E2E query client is unavailable.");
|
|
return (client.getQueryData(["channel-messages", channelId]) ?? []).map(
|
|
(event) => event.id,
|
|
);
|
|
}, DM_ID);
|
|
}
|
|
|
|
function isDmWindowQuery(route: Route) {
|
|
const request = route.request();
|
|
if (request.method() !== "POST" || !request.url().endsWith("/query")) {
|
|
return false;
|
|
}
|
|
try {
|
|
const body = request.postDataJSON() as Array<Record<string, unknown>>;
|
|
return body.some(
|
|
(filter) =>
|
|
Array.isArray(filter["#h"]) &&
|
|
filter["#h"].includes(DM_ID) &&
|
|
filter.top_level === true &&
|
|
filter.include_summaries === true &&
|
|
filter.include_aux === true,
|
|
);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function dmLiveRequestCount(page: Page) {
|
|
return page.evaluate((channelId) => {
|
|
let count = 0;
|
|
for (const entry of window.__BUZZ_E2E_COMMAND_LOG__ ?? []) {
|
|
if (entry.command !== "plugin:websocket|send") continue;
|
|
const data = (
|
|
entry.payload as { message?: { data?: string } } | undefined
|
|
)?.message?.data;
|
|
if (!data) continue;
|
|
try {
|
|
const frame = JSON.parse(data) as [
|
|
string,
|
|
string,
|
|
...Record<string, unknown>[],
|
|
];
|
|
if (
|
|
frame[0] === "REQ" &&
|
|
frame
|
|
.slice(2)
|
|
.some(
|
|
(filter) =>
|
|
Array.isArray(filter["#h"]) &&
|
|
filter["#h"].includes(channelId) &&
|
|
typeof filter.since === "number",
|
|
)
|
|
) {
|
|
count += 1;
|
|
}
|
|
} catch {
|
|
// Ignore non-JSON websocket payloads.
|
|
}
|
|
}
|
|
return count;
|
|
}, DM_ID);
|
|
}
|
|
|
|
test.beforeAll(async () => {
|
|
test.setTimeout(90_000);
|
|
await assertRelaySeeded();
|
|
});
|
|
|
|
test("existing DM history remains when the first live DM reaches a pageless window", async ({
|
|
page,
|
|
}) => {
|
|
const baseSecond = Math.floor(Date.now() / 1000) - 10;
|
|
const history = await Promise.all(
|
|
[0, 1, 2].map((index) =>
|
|
publishAliceDm(`dm history ${index}`, baseSecond + index),
|
|
),
|
|
);
|
|
const historyIds = history.map((event) => event.id);
|
|
|
|
await installRelayBridge(page, "tyler");
|
|
await page.goto("/");
|
|
await page.getByTestId("channel-alice-tyler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
|
|
await expect.poll(() => timelineIds(page)).toEqual(historyIds);
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
await expect.poll(() => cachedMessageIds(page)).toEqual(historyIds);
|
|
await page.evaluate((channelId) => {
|
|
const client = window.__BUZZ_E2E_QUERY_CLIENT__ as unknown as {
|
|
removeQueries: (filters: {
|
|
queryKey: readonly unknown[];
|
|
exact: boolean;
|
|
}) => void;
|
|
};
|
|
if (!client) throw new Error("E2E query client is unavailable.");
|
|
client.removeQueries({
|
|
queryKey: ["channel-window", channelId],
|
|
exact: true,
|
|
});
|
|
}, DM_ID);
|
|
|
|
let heldDmWindowQuery = false;
|
|
let releaseHeldQuery: (() => void) | undefined;
|
|
let finishHeldQuery: (() => void) | undefined;
|
|
const heldQueryReleased = new Promise<void>((resolve) => {
|
|
releaseHeldQuery = resolve;
|
|
});
|
|
const heldQueryFinished = new Promise<void>((resolve) => {
|
|
finishHeldQuery = resolve;
|
|
});
|
|
const holdDmWindowQuery = async (route: Route) => {
|
|
if (!isDmWindowQuery(route)) {
|
|
await route.continue();
|
|
return;
|
|
}
|
|
heldDmWindowQuery = true;
|
|
await heldQueryReleased;
|
|
await route.continue();
|
|
finishHeldQuery?.();
|
|
};
|
|
await page.route("**/query", holdDmWindowQuery);
|
|
|
|
try {
|
|
const priorLiveRequests = await dmLiveRequestCount(page);
|
|
await page.getByTestId("channel-alice-tyler").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
|
|
await expect
|
|
.poll(() => dmLiveRequestCount(page))
|
|
.toBeGreaterThan(priorLiveRequests);
|
|
|
|
const live = await publishAliceDm(
|
|
"dm live arrival",
|
|
Math.floor(Date.now() / 1000),
|
|
);
|
|
const expectedIds = [...historyIds, live.id];
|
|
|
|
await expect.poll(() => timelineIds(page)).toEqual(expectedIds);
|
|
await expect.poll(() => cachedMessageIds(page)).toEqual(expectedIds);
|
|
expect(new Set(expectedIds).size).toBe(expectedIds.length);
|
|
await expect(page.getByTestId("channel-alice-tyler")).toBeVisible();
|
|
await expect(page.getByTestId("message-input")).toBeVisible();
|
|
await expect(page.getByTestId("message-input")).toBeEnabled();
|
|
} finally {
|
|
releaseHeldQuery?.();
|
|
if (heldDmWindowQuery) await heldQueryFinished;
|
|
await page.unroute("**/query", holdDmWindowQuery);
|
|
}
|
|
});
|