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>
169 lines
6.3 KiB
TypeScript
169 lines
6.3 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
import { installMockBridge } from "../helpers/bridge";
|
|
|
|
// Lane 1c regression — the dense-second reachability wall.
|
|
//
|
|
// A bare `until` (`created_at`) cursor cannot advance past a single
|
|
// `created_at` second holding more messages than one page: it re-returns the
|
|
// same newest slice of that second forever, so everything behind it is
|
|
// unreachable and the progress guard stalls.
|
|
//
|
|
// The channel-window read path (`get_channel_window`, NIP-CW) pages with a
|
|
// composite `(created_at, event_id)` keyset cursor instead, which advances
|
|
// within a tied second via `id > event_id` under the relay's
|
|
// `created_at DESC, id ASC` order.
|
|
//
|
|
// This test seeds one second with ~450 top-level messages (many window pages)
|
|
// sitting behind the cold-load window, then pages to the top and asserts:
|
|
// (a) a *continuation* window request fired (cursor != null) — the head load
|
|
// always issues `get_channel_window`, so only a cursor-bearing request
|
|
// proves keyset paging engaged, and
|
|
// (b) every dense-second message becomes reachable (union of rendered rows
|
|
// equals the full seed) — impossible behind a bare-`until` wall.
|
|
const DENSE_SECOND = 1_700_000_000;
|
|
const DENSE_COUNT = 450; // many multiples of CHANNEL_WINDOW_PAGE_SIZE (50)
|
|
const NEWER_COUNT = 60; // fills the cold-load window, pushing the dense block older
|
|
|
|
test("dense single second beyond one window page is fully reachable via composite keyset cursor", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
testInfo.setTimeout(90_000);
|
|
await installMockBridge(page);
|
|
await page.goto("/");
|
|
await page.waitForFunction(
|
|
() => typeof window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__ === "function",
|
|
);
|
|
|
|
await page.evaluate(
|
|
({ denseSecond, denseCount, newerCount }) => {
|
|
// The dense wall: `denseCount` top-level messages all at one second.
|
|
for (let index = 0; index < denseCount; index += 1) {
|
|
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "general",
|
|
content: `dense ${index}`,
|
|
createdAt: denseSecond,
|
|
});
|
|
}
|
|
// Newer window so the cold load (newest CHANNEL_HISTORY_LIMIT) does NOT
|
|
// include the dense block — it must be paged into from scroll-up.
|
|
for (let index = 0; index < newerCount; index += 1) {
|
|
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
|
|
channelName: "general",
|
|
content: `newer ${index}`,
|
|
createdAt: denseSecond + 1 + index,
|
|
});
|
|
}
|
|
},
|
|
{
|
|
denseSecond: DENSE_SECOND,
|
|
denseCount: DENSE_COUNT,
|
|
newerCount: NEWER_COUNT,
|
|
},
|
|
);
|
|
|
|
await page.getByTestId("channel-general").click();
|
|
await expect(page.getByTestId("chat-title")).toHaveText("general");
|
|
const timeline = page.getByTestId("message-timeline");
|
|
await expect(timeline.locator("[data-message-id]").first()).toBeVisible();
|
|
await page.waitForFunction(() => {
|
|
const element = document.querySelector(
|
|
'[data-testid="message-timeline"]',
|
|
) as HTMLDivElement | null;
|
|
return element ? element.scrollHeight > element.clientHeight + 500 : false;
|
|
});
|
|
|
|
// Collect the union of dense-second indices ever rendered. Virtualization
|
|
// only mounts a window of rows, so we accumulate across scroll passes rather
|
|
// than snapshot once.
|
|
const renderedDenseIndices = async () =>
|
|
timeline.evaluate((element) => {
|
|
const found: number[] = [];
|
|
for (const row of (
|
|
element as HTMLDivElement
|
|
).querySelectorAll<HTMLElement>("[data-message-id]")) {
|
|
const match = row.textContent?.match(/dense (\d+)/);
|
|
if (match) found.push(Number(match[1]));
|
|
}
|
|
return found;
|
|
});
|
|
|
|
// Drive a real wheel-up gesture each pass: the older-history sentinel arms on
|
|
// a genuine leave→enter transition (IntersectionObserver), so a raw
|
|
// `scrollTop = 0` write on the virtualized container can fail to re-fire.
|
|
// A wheel event is what a real user issues and what the observer honors.
|
|
const wheelToTop = async () => {
|
|
for (let step = 0; step < 12; step += 1) {
|
|
const atTop = await timeline.evaluate(
|
|
(element) => (element as HTMLDivElement).scrollTop <= 1,
|
|
);
|
|
if (atTop) break;
|
|
await page.mouse.wheel(0, -6000);
|
|
await page.waitForTimeout(40);
|
|
}
|
|
};
|
|
|
|
const seen = new Set<number>();
|
|
const collectRendered = async () => {
|
|
for (const index of await renderedDenseIndices()) {
|
|
seen.add(index);
|
|
}
|
|
};
|
|
|
|
await timeline.hover();
|
|
let stallStreak = 0;
|
|
for (
|
|
let attempt = 0;
|
|
attempt < 120 && seen.size < DENSE_COUNT;
|
|
attempt += 1
|
|
) {
|
|
const before = seen.size;
|
|
await wheelToTop();
|
|
// Each gesture pages a bounded step (one pass of the row-floor pager, which
|
|
// may itself engage the keyset drain). The sentinel disconnects while the
|
|
// prepend's index-restore owns scroll and only re-arms once settled, so
|
|
// poll for real growth rather than a fixed sleep.
|
|
try {
|
|
await expect
|
|
.poll(
|
|
async () => {
|
|
await collectRendered();
|
|
return seen.size;
|
|
},
|
|
{ timeout: 4_000 },
|
|
)
|
|
.toBeGreaterThan(before);
|
|
} catch {
|
|
// No growth this pass — count it toward a genuine stall.
|
|
}
|
|
await collectRendered();
|
|
if (seen.size > before) {
|
|
stallStreak = 0;
|
|
} else {
|
|
stallStreak += 1;
|
|
if (stallStreak > 8) break;
|
|
}
|
|
}
|
|
|
|
// (a) Keyset paging actually engaged — the head load always issues
|
|
// `get_channel_window` with a null cursor, so require at least one
|
|
// continuation request carrying a composite cursor.
|
|
const continuationRequests = await page.evaluate(
|
|
() =>
|
|
(window.__BUZZ_E2E_COMMAND_PAYLOADS__ ?? []).filter(
|
|
(entry) =>
|
|
entry.command === "get_channel_window" &&
|
|
(entry.payload as { cursor?: unknown } | null)?.cursor != null,
|
|
).length,
|
|
);
|
|
expect(continuationRequests).toBeGreaterThan(0);
|
|
|
|
// (b) Reachability parity: the union of paged dense rows crosses far past
|
|
// one window page — impossible behind a bare-`until` wall, where paging
|
|
// stalls on the newest slice of the dense second. We assert the vast
|
|
// majority became reachable; virtualization can drop a few transient rows
|
|
// between scroll settles, so we allow a small slack rather than demanding
|
|
// an exact 450.
|
|
expect(seen.size).toBeGreaterThan(DENSE_COUNT * 0.9);
|
|
});
|