mirror of
https://github.com/block/buzz.git
synced 2026-10-27 04:30:52 +00:00
## Summary - persist each relay/identity's complete channel list and server hash as one integrity-checked snapshot - paint the snapshot immediately on cold boot, then revalidate with `knownHash` - fail slow-never-wrong: malformed/legacy/partial snapshots and mismatched not-modified responses force an unhashed full fetch - add sidebar boot diagnostics and deterministic unit/E2E coverage for boot, identity/relay isolation, partial writes, mismatch fallback, and community switches ## Safety invariants - channel list and hash are serialized in one localStorage document and replaced together - snapshot ownership is scoped to normalized relay URL plus identity pubkey - a not-modified response is accepted only when its hash exactly matches the hash describing the available list - any missing or impossible hash/list pairing retries `getChannels(null)` before replacing persistence ## Validation At exact commit `19ca25d23c434cc0b8893a93691aaf4c77794f60` with a clean working tree: - `cd desktop && pnpm check && pnpm typecheck` — passed (existing informational Biome findings only) - `cd desktop && pnpm test` — 4,723 passed - `cd desktop && node --import ./test-loader.mjs --experimental-strip-types --test src/features/channels/channelSnapshot.test.mjs` — 13 passed - `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts --grep "cold boot paints" --repeat-each=5` — 5 passed - `cd desktop && pnpm exec playwright test e2e/sidebar-snapshot.spec.ts` — 8 passed - push hooks repeated desktop check/typecheck and all 4,723 unit tests successfully The Playwright suite uses injected bridge delays. Its roughly 0.5–0.6 s snapshot paint and 3.0 s snapshot-to-live readings are synthetic invariant evidence, not production desktop performance measurements. ## Measurement context The controlled current-main investigation is documented separately in `RESEARCH/DESKTOP_PERF_DEEP_DIVE_2026_08_12.md`; its raw local artifacts are `.scratch/summer-perf-deepdive-results-v2.json`, `.scratch/summer-perf-deepdive-run.log`, `.scratch/summer-perf-deepdive-run-2.log`, and `.scratch/summer-perf-deepdive-build-2.log`. Those original timings are also synthetic Chromium/mock-bridge measurements and are not presented as shipped Tauri/WKWebView or production-relay numbers. ## Latest review delta At exact tip `e8e2b1d617aac7ea008258ad9974bbf8da9cd2eb`, storage-denial reads fail open to the live fetch, hashless retries reject `channels: null` before pair/persistence updates, identity-read failure enables a hashless live fetch, and repeated consumers reuse snapshot parsing/integrity validation by storage key + raw document. The four remaining review nits are deferred as non-blocking follow-ups. Validation at this tip: sidebar snapshot E2E 30/30 serial; desktop unit suite 4,725/4,725; full push gate green (desktop check/typecheck/unit, Rust, Tauri). --------- Signed-off-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz> Co-authored-by: Summer <1fdd3cc104e2911eb3b2da6f97d1b25f4a7f3550ded4492b24ff1d95acd66766@buzz.block.builderlab.xyz>
600 lines
18 KiB
TypeScript
600 lines
18 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
|
|
const RELAY_URL = "ws://localhost:3000";
|
|
const OTHER_RELAY_URL = "ws://localhost:3001";
|
|
const OWNER_PUBKEY = "deadbeef".repeat(8);
|
|
const STALE_COMMUNITY_PUBKEY = "cafebabe".repeat(8);
|
|
const MATCHING_HASH = "mock-hash";
|
|
const READ_DELAY_MS = 600;
|
|
const SNAPSHOT_FRAME_DELAY_MS = 3_000;
|
|
|
|
function snapshotKey(relayUrl: string, ownerPubkey = OWNER_PUBKEY) {
|
|
return `buzz-channels.v1:${relayUrl}:${ownerPubkey.toLowerCase()}`;
|
|
}
|
|
|
|
function makeSnapshotChannel(index: number, prefix = "snapshot") {
|
|
return {
|
|
id: `${prefix}-${index}`,
|
|
name: `${prefix}-${String(index).padStart(2, "0")}`,
|
|
channelType: "stream",
|
|
visibility: "open",
|
|
description: "",
|
|
topic: null,
|
|
purpose: null,
|
|
memberCount: 1,
|
|
memberPubkeys: [OWNER_PUBKEY],
|
|
lastMessageAt: null,
|
|
archivedAt: null,
|
|
participants: [],
|
|
participantPubkeys: [],
|
|
isMember: true,
|
|
ttlSeconds: null,
|
|
ttlDeadline: null,
|
|
};
|
|
}
|
|
|
|
const FULL_SNAPSHOT = Array.from({ length: 14 }, (_, index) =>
|
|
makeSnapshotChannel(index),
|
|
);
|
|
const SNAPSHOT_DIAGNOSTIC_MARK = "buzz:sidebar:snapshot-diagnostic";
|
|
const FULL_SIDEBAR_PAINT_MARK = "buzz:sidebar:full-list-painted";
|
|
const BOOT_TO_FULL_SIDEBAR_MEASURE = "buzz:sidebar:boot-to-full-list-painted";
|
|
|
|
function snapshotIntegrity(
|
|
ownerPubkey: string,
|
|
hash: string,
|
|
channels: ReturnType<typeof makeSnapshotChannel>[],
|
|
) {
|
|
const value = JSON.stringify([ownerPubkey.toLowerCase(), hash, channels]);
|
|
let result = 0x811c9dc5;
|
|
for (let index = 0; index < value.length; index += 1) {
|
|
result ^= value.charCodeAt(index);
|
|
result = Math.imul(result, 0x01000193);
|
|
}
|
|
return (result >>> 0).toString(16).padStart(8, "0");
|
|
}
|
|
|
|
type SnapshotDiagnostics = {
|
|
ageMs: number;
|
|
channelCount: number;
|
|
presence: "absent" | "invalid" | "present";
|
|
serializedBytes: number;
|
|
};
|
|
|
|
async function getSnapshotDiagnostic(page: Page) {
|
|
return page.evaluate((name) => {
|
|
const entry = performance.getEntriesByName(name).at(-1) as
|
|
| PerformanceMark
|
|
| undefined;
|
|
return entry?.detail as SnapshotDiagnostics | undefined;
|
|
}, SNAPSHOT_DIAGNOSTIC_MARK);
|
|
}
|
|
|
|
async function getFullSidebarMeasure(page: Page) {
|
|
return page.evaluate(
|
|
({ markName, measureName }) => ({
|
|
markCount: performance.getEntriesByName(markName).length,
|
|
measure: performance.getEntriesByName(measureName).at(-1)?.duration,
|
|
}),
|
|
{
|
|
markName: FULL_SIDEBAR_PAINT_MARK,
|
|
measureName: BOOT_TO_FULL_SIDEBAR_MEASURE,
|
|
},
|
|
);
|
|
}
|
|
|
|
async function seedSnapshot(
|
|
page: Page,
|
|
options: {
|
|
channels?: ReturnType<typeof makeSnapshotChannel>[];
|
|
hash: string;
|
|
keyOwnerPubkey?: string;
|
|
ownerPubkey?: string;
|
|
relayUrl?: string;
|
|
},
|
|
) {
|
|
const channels = options.channels ?? FULL_SNAPSHOT;
|
|
const ownerPubkey = options.ownerPubkey ?? OWNER_PUBKEY;
|
|
await page.addInitScript(
|
|
({ channels, hash, integrity, key, ownerPubkey }) => {
|
|
window.localStorage.setItem(
|
|
key,
|
|
JSON.stringify({
|
|
version: 2,
|
|
updatedAt: Date.now(),
|
|
ownerPubkey,
|
|
hash,
|
|
channels,
|
|
integrity,
|
|
}),
|
|
);
|
|
},
|
|
{
|
|
channels,
|
|
hash: options.hash,
|
|
integrity: snapshotIntegrity(ownerPubkey, options.hash, channels),
|
|
key: snapshotKey(
|
|
options.relayUrl ?? RELAY_URL,
|
|
options.keyOwnerPubkey ?? ownerPubkey,
|
|
),
|
|
ownerPubkey,
|
|
},
|
|
);
|
|
}
|
|
|
|
async function seedCommunities(page: Page, communityPubkey = OWNER_PUBKEY) {
|
|
await page.addInitScript(
|
|
({ communityPubkey, relayA, relayB }) => {
|
|
const communities = [
|
|
{
|
|
id: "community-a",
|
|
name: "Alpha",
|
|
relayUrl: relayA,
|
|
pubkey: communityPubkey,
|
|
addedAt: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
{
|
|
id: "community-b",
|
|
name: "Bravo",
|
|
relayUrl: relayB,
|
|
pubkey: communityPubkey,
|
|
addedAt: "2026-01-02T00:00:00.000Z",
|
|
},
|
|
];
|
|
window.localStorage.setItem(
|
|
"buzz-communities",
|
|
JSON.stringify(communities),
|
|
);
|
|
window.localStorage.setItem(
|
|
"buzz-active-community-id",
|
|
communities[0].id,
|
|
);
|
|
},
|
|
{ communityPubkey, relayA: RELAY_URL, relayB: OTHER_RELAY_URL },
|
|
);
|
|
}
|
|
|
|
async function getChannelsPayloads(page: Page) {
|
|
return page.evaluate(() =>
|
|
(window.__BUZZ_E2E_COMMAND_LOG__ ?? [])
|
|
.filter((entry) => entry.command === "get_channels")
|
|
.map((entry) => entry.payload as { knownHash?: unknown }),
|
|
);
|
|
}
|
|
|
|
async function mutateDisplayedChannels(page: Page, channelName: string) {
|
|
await page.evaluate((name) => {
|
|
const queryClient = window.__BUZZ_E2E_QUERY_CLIENT__ as
|
|
| {
|
|
setQueryData: (
|
|
queryKey: readonly string[],
|
|
updater: (
|
|
channels: ReturnType<typeof makeSnapshotChannel>[] | undefined,
|
|
) => ReturnType<typeof makeSnapshotChannel>[],
|
|
) => unknown;
|
|
}
|
|
| undefined;
|
|
if (!queryClient) {
|
|
throw new Error("E2E query client seam is not installed.");
|
|
}
|
|
queryClient.setQueryData(["channels"], (channels) => {
|
|
const template = channels?.[0];
|
|
if (!template) {
|
|
throw new Error("Displayed channel cache is empty.");
|
|
}
|
|
return [...channels, { ...template, id: "optimistic-channel", name }];
|
|
});
|
|
}, channelName);
|
|
}
|
|
|
|
async function readPersistedSnapshot(page: Page) {
|
|
return page.evaluate((key) => {
|
|
const raw = window.localStorage.getItem(key);
|
|
if (!raw) return null;
|
|
const snapshot = JSON.parse(raw) as {
|
|
channels: Array<{ name: string }>;
|
|
hash: string;
|
|
};
|
|
return {
|
|
channelNames: snapshot.channels.map((channel) => channel.name),
|
|
hash: snapshot.hash,
|
|
};
|
|
}, snapshotKey(RELAY_URL));
|
|
}
|
|
|
|
async function trackSnapshotRows(page: Page) {
|
|
await page.addInitScript(() => {
|
|
const testWindow = window as Window & {
|
|
__BUZZ_E2E_SNAPSHOT_ROWS_SEEN__?: string[];
|
|
};
|
|
const seen: string[] = [];
|
|
testWindow.__BUZZ_E2E_SNAPSHOT_ROWS_SEEN__ = seen;
|
|
const record = (node: Node) => {
|
|
if (!(node instanceof Element)) return;
|
|
const rows = node.matches('[data-channel-id^="snapshot-"]')
|
|
? [node]
|
|
: Array.from(node.querySelectorAll('[data-channel-id^="snapshot-"]'));
|
|
for (const row of rows) {
|
|
const id = row.getAttribute("data-channel-id");
|
|
if (id && !seen.includes(id)) seen.push(id);
|
|
}
|
|
};
|
|
new MutationObserver((records) => {
|
|
for (const mutation of records) {
|
|
for (const node of mutation.addedNodes) record(node);
|
|
}
|
|
}).observe(document, { childList: true, subtree: true });
|
|
});
|
|
}
|
|
|
|
async function getTrackedSnapshotRows(page: Page) {
|
|
return page.evaluate(
|
|
() =>
|
|
(
|
|
window as Window & {
|
|
__BUZZ_E2E_SNAPSHOT_ROWS_SEEN__?: string[];
|
|
}
|
|
).__BUZZ_E2E_SNAPSHOT_ROWS_SEEN__ ?? [],
|
|
);
|
|
}
|
|
|
|
test("cold boot paints the complete snapshot, sends its hash, and revalidates", async ({
|
|
page,
|
|
}) => {
|
|
await seedSnapshot(page, { hash: MATCHING_HASH });
|
|
await installMockBridge(page, {
|
|
channelsReadDelayMs: SNAPSHOT_FRAME_DELAY_MS,
|
|
honorChannelsKnownHash: true,
|
|
});
|
|
|
|
const navigationStartedAt = performance.now();
|
|
await page.goto("/");
|
|
const rows = page.locator('[data-channel-id^="snapshot-"]');
|
|
await expect(rows).toHaveCount(FULL_SNAPSHOT.length, { timeout: 2_000 });
|
|
const snapshotPaintedAt = performance.now();
|
|
expect(snapshotPaintedAt - navigationStartedAt).toBeLessThan(
|
|
SNAPSHOT_FRAME_DELAY_MS,
|
|
);
|
|
await expect(page.getByTestId("sidebar-loading")).toHaveCount(0);
|
|
await expect
|
|
.poll(() => getChannelsPayloads(page))
|
|
.toEqual([{ knownHash: MATCHING_HASH }]);
|
|
await expect
|
|
.poll(() => getSnapshotDiagnostic(page))
|
|
.toMatchObject({
|
|
channelCount: FULL_SNAPSHOT.length,
|
|
presence: "present",
|
|
});
|
|
const diagnostics = await getSnapshotDiagnostic(page);
|
|
expect(diagnostics?.serializedBytes).toBeGreaterThan(0);
|
|
expect(diagnostics?.ageMs).toBeGreaterThanOrEqual(0);
|
|
|
|
// The persisted data is deliberately stale: it paints immediately but still
|
|
// validates. The delayed matching response leaves a deterministic snapshot
|
|
// frame, then not-modified must not shrink the list.
|
|
await expect(rows).toHaveCount(FULL_SNAPSHOT.length);
|
|
await expect
|
|
.poll(() => getFullSidebarMeasure(page))
|
|
.toEqual({
|
|
markCount: expect.any(Number),
|
|
measure: expect.any(Number),
|
|
});
|
|
expect((await getFullSidebarMeasure(page)).markCount).toBeGreaterThan(0);
|
|
|
|
const liveSettledAt = performance.now();
|
|
console.info(
|
|
`[sidebar-snapshot] firstPaint=${Math.round(snapshotPaintedAt - navigationStartedAt)}ms snapshotToLive=${Math.round(liveSettledAt - snapshotPaintedAt)}ms rows=${FULL_SNAPSHOT.length}`,
|
|
);
|
|
});
|
|
|
|
test("matching not-modified preserves display mutations without persisting them", async ({
|
|
page,
|
|
}) => {
|
|
const optimisticName = "optimistic-local-channel";
|
|
await seedSnapshot(page, { hash: MATCHING_HASH });
|
|
await installMockBridge(page, {
|
|
channelsReadDelayMs: READ_DELAY_MS,
|
|
honorChannelsKnownHash: true,
|
|
});
|
|
await page.goto("/");
|
|
|
|
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(
|
|
FULL_SNAPSHOT.length,
|
|
{ timeout: 500 },
|
|
);
|
|
await mutateDisplayedChannels(page, optimisticName);
|
|
await expect(
|
|
page.locator('[data-channel-id="optimistic-channel"]'),
|
|
).toBeVisible();
|
|
|
|
await expect
|
|
.poll(() => getChannelsPayloads(page))
|
|
.toEqual([{ knownHash: MATCHING_HASH }]);
|
|
await expect
|
|
.poll(() => readPersistedSnapshot(page))
|
|
.toEqual({
|
|
channelNames: FULL_SNAPSHOT.map((channel) => channel.name),
|
|
hash: MATCHING_HASH,
|
|
});
|
|
await expect(
|
|
page.locator('[data-channel-id="optimistic-channel"]'),
|
|
).toBeVisible();
|
|
expect((await readPersistedSnapshot(page))?.channelNames).not.toContain(
|
|
optimisticName,
|
|
);
|
|
});
|
|
|
|
test("first-ever boot without a snapshot sends null and shows loading", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, { channelsReadDelayMs: READ_DELAY_MS });
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("sidebar-loading")).toBeVisible({
|
|
timeout: 500,
|
|
});
|
|
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
|
|
await expect
|
|
.poll(() => getChannelsPayloads(page))
|
|
.toEqual([{ knownHash: null }]);
|
|
await expect
|
|
.poll(() => getSnapshotDiagnostic(page))
|
|
.toMatchObject({
|
|
channelCount: 0,
|
|
presence: "absent",
|
|
serializedBytes: 0,
|
|
});
|
|
await expect(page.getByTestId("channel-general")).toBeVisible();
|
|
await expect
|
|
.poll(() => getFullSidebarMeasure(page))
|
|
.toEqual({
|
|
markCount: expect.any(Number),
|
|
measure: expect.any(Number),
|
|
});
|
|
expect((await getFullSidebarMeasure(page)).markCount).toBeGreaterThan(0);
|
|
});
|
|
|
|
test("a different identity's snapshot is ignored", async ({ page }) => {
|
|
await seedSnapshot(page, {
|
|
hash: "foreign-hash",
|
|
keyOwnerPubkey: OWNER_PUBKEY,
|
|
ownerPubkey: STALE_COMMUNITY_PUBKEY,
|
|
});
|
|
await installMockBridge(page, { channelsReadDelayMs: READ_DELAY_MS });
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("sidebar-loading")).toBeVisible({
|
|
timeout: 500,
|
|
});
|
|
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
|
|
await expect
|
|
.poll(() => getChannelsPayloads(page))
|
|
.toEqual([{ knownHash: null }]);
|
|
await expect
|
|
.poll(() => getSnapshotDiagnostic(page))
|
|
.toMatchObject({
|
|
channelCount: 0,
|
|
presence: "invalid",
|
|
});
|
|
await expect(page.getByTestId("channel-general")).toBeVisible();
|
|
});
|
|
|
|
test("display-only community pubkey cannot expose another identity's snapshot", async ({
|
|
page,
|
|
}) => {
|
|
await seedSnapshot(page, {
|
|
hash: "stale-community-identity-hash",
|
|
ownerPubkey: STALE_COMMUNITY_PUBKEY,
|
|
});
|
|
await trackSnapshotRows(page);
|
|
await installMockBridge(
|
|
page,
|
|
{ channelsReadDelayMs: READ_DELAY_MS },
|
|
{ skipCommunitySeed: true },
|
|
);
|
|
await seedCommunities(page, STALE_COMMUNITY_PUBKEY);
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("channel-general")).toBeVisible();
|
|
expect(await getTrackedSnapshotRows(page)).toEqual([]);
|
|
const payloads = await getChannelsPayloads(page);
|
|
expect(payloads).toEqual([{ knownHash: null }]);
|
|
expect(payloads).not.toContainEqual({
|
|
knownHash: "stale-community-identity-hash",
|
|
});
|
|
});
|
|
|
|
test("partial hash/list write fails toward a full fetch", async ({ page }) => {
|
|
await seedSnapshot(page, { hash: MATCHING_HASH });
|
|
await page.addInitScript((key) => {
|
|
const raw = window.localStorage.getItem(key);
|
|
if (!raw) return;
|
|
const snapshot = JSON.parse(raw) as { hash: string };
|
|
window.localStorage.setItem(
|
|
key,
|
|
JSON.stringify({ ...snapshot, hash: "newer-partial-hash" }),
|
|
);
|
|
}, snapshotKey(RELAY_URL));
|
|
await installMockBridge(page, {
|
|
channelsReadDelayMs: READ_DELAY_MS,
|
|
honorChannelsKnownHash: true,
|
|
});
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("sidebar-loading")).toBeVisible({
|
|
timeout: 500,
|
|
});
|
|
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
|
|
await expect
|
|
.poll(() => getChannelsPayloads(page))
|
|
.toEqual([{ knownHash: null }]);
|
|
await expect
|
|
.poll(() => getSnapshotDiagnostic(page))
|
|
.toMatchObject({
|
|
channelCount: 0,
|
|
presence: "invalid",
|
|
});
|
|
await expect(page.getByTestId("channel-general")).toBeVisible();
|
|
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
|
|
});
|
|
|
|
test("unexpected not-modified response retries without a hash", async ({
|
|
page,
|
|
}) => {
|
|
await installMockBridge(page, {
|
|
channelsNotModifiedResponses: 1,
|
|
});
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByTestId("channel-general")).toBeVisible();
|
|
await expect
|
|
.poll(() => getChannelsPayloads(page))
|
|
.toEqual([{ knownHash: null }, { knownHash: null }]);
|
|
});
|
|
|
|
test("mismatched not-modified hash falls back to a full list", async ({
|
|
page,
|
|
}) => {
|
|
await seedSnapshot(page, { hash: "persisted-stale-hash" });
|
|
await installMockBridge(page, {
|
|
channelsReadDelayMs: READ_DELAY_MS,
|
|
channelsNotModifiedResponses: 1,
|
|
});
|
|
await page.goto("/");
|
|
|
|
const snapshotRows = page.locator('[data-channel-id^="snapshot-"]');
|
|
await expect(snapshotRows).toHaveCount(FULL_SNAPSHOT.length, {
|
|
timeout: 500,
|
|
});
|
|
await expect
|
|
.poll(() => getChannelsPayloads(page))
|
|
.toEqual([{ knownHash: "persisted-stale-hash" }, { knownHash: null }]);
|
|
|
|
// The stale snapshot may provide the immediate boot frame, but an impossible
|
|
// not-modified hash/list pairing must never survive as the settled result.
|
|
await expect(page.getByTestId("channel-general")).toBeVisible();
|
|
await expect(snapshotRows).toHaveCount(0);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate((key) => {
|
|
const raw = window.localStorage.getItem(key);
|
|
if (!raw) return null;
|
|
const snapshot = JSON.parse(raw) as {
|
|
channels: Array<{ name: string }>;
|
|
hash: string;
|
|
};
|
|
return {
|
|
hash: snapshot.hash,
|
|
hasGeneral: snapshot.channels.some(
|
|
(channel) => channel.name === "general",
|
|
),
|
|
hasSnapshotRow: snapshot.channels.some((channel) =>
|
|
channel.name.startsWith("snapshot-"),
|
|
),
|
|
};
|
|
}, snapshotKey(RELAY_URL)),
|
|
)
|
|
.toEqual({ hash: MATCHING_HASH, hasGeneral: true, hasSnapshotRow: false });
|
|
});
|
|
|
|
test("hash mismatch replaces the snapshot with the full live list", async ({
|
|
page,
|
|
}) => {
|
|
await seedSnapshot(page, { hash: "stale-hash" });
|
|
await installMockBridge(page, {
|
|
channelsReadDelayMs: READ_DELAY_MS,
|
|
honorChannelsKnownHash: true,
|
|
});
|
|
await page.goto("/");
|
|
|
|
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(
|
|
FULL_SNAPSHOT.length,
|
|
{ timeout: 500 },
|
|
);
|
|
await expect
|
|
.poll(() => getChannelsPayloads(page))
|
|
.toEqual([{ knownHash: "stale-hash" }]);
|
|
await expect(page.getByTestId("channel-general")).toBeVisible();
|
|
await expect(page.locator('[data-channel-id^="snapshot-"]')).toHaveCount(0);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate((key) => {
|
|
const raw = window.localStorage.getItem(key);
|
|
if (!raw) return null;
|
|
const snapshot = JSON.parse(raw) as {
|
|
channels: Array<{ name: string }>;
|
|
hash: string;
|
|
};
|
|
return {
|
|
hash: snapshot.hash,
|
|
hasGeneral: snapshot.channels.some(
|
|
(channel) => channel.name === "general",
|
|
),
|
|
hasSnapshotRow: snapshot.channels.some((channel) =>
|
|
channel.name.startsWith("snapshot-"),
|
|
),
|
|
};
|
|
}, snapshotKey(RELAY_URL)),
|
|
)
|
|
.toEqual({ hash: MATCHING_HASH, hasGeneral: true, hasSnapshotRow: false });
|
|
});
|
|
|
|
test("community switch validates a stale relay snapshot and replaces it", async ({
|
|
page,
|
|
}) => {
|
|
const switchedSnapshot = Array.from({ length: 6 }, (_, index) =>
|
|
makeSnapshotChannel(index, "switched"),
|
|
);
|
|
await seedSnapshot(page, {
|
|
channels: switchedSnapshot,
|
|
hash: "stale-community-hash",
|
|
relayUrl: OTHER_RELAY_URL,
|
|
});
|
|
await installMockBridge(
|
|
page,
|
|
{
|
|
channelsReadDelayMs: READ_DELAY_MS,
|
|
honorChannelsKnownHash: true,
|
|
},
|
|
{ skipCommunitySeed: true },
|
|
);
|
|
await seedCommunities(page);
|
|
await page.goto("/");
|
|
await expect(page.getByTestId("app-sidebar")).toBeVisible();
|
|
|
|
const payloadCountBeforeSwitch = (await getChannelsPayloads(page)).length;
|
|
await page.getByTestId("community-rail-button-community-b").click();
|
|
const switchedRows = page.locator('[data-channel-id^="switched-"]');
|
|
await expect(switchedRows).toHaveCount(switchedSnapshot.length, {
|
|
timeout: 500,
|
|
});
|
|
await expect
|
|
.poll(async () =>
|
|
(await getChannelsPayloads(page)).slice(payloadCountBeforeSwitch).at(0),
|
|
)
|
|
.toEqual({ knownHash: "stale-community-hash" });
|
|
|
|
await expect(page.getByTestId("channel-general")).toBeVisible();
|
|
await expect(switchedRows).toHaveCount(0);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate((key) => {
|
|
const raw = window.localStorage.getItem(key);
|
|
if (!raw) return null;
|
|
const snapshot = JSON.parse(raw) as {
|
|
channels: Array<{ name: string }>;
|
|
hash: string;
|
|
};
|
|
return {
|
|
hash: snapshot.hash,
|
|
hasGeneral: snapshot.channels.some(
|
|
(channel) => channel.name === "general",
|
|
),
|
|
};
|
|
}, snapshotKey(OTHER_RELAY_URL)),
|
|
)
|
|
.toEqual({ hash: MATCHING_HASH, hasGeneral: true });
|
|
});
|