feat: import Chinese-localized Buzz source snapshot
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
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>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { installRelayBridge } from "../helpers/bridge";
|
||||
import { assertRelaySeeded } from "../helpers/seed";
|
||||
import { ancestorIsland, seedScenario } from "../helpers/seedRelay";
|
||||
|
||||
// =============================================================================
|
||||
// LIVE-RELAY parity — ancestor-island cursor poisoning (GUI read-model overhaul)
|
||||
// =============================================================================
|
||||
//
|
||||
// The deterministic current-main RED (Dawn's Jul-2 root cause, Wren's Jul-3
|
||||
// proven repro `239cc161`). The dense-second wall is NOT the disease — main
|
||||
// already drains a tied second via the PR #1418 keyset fallback. THIS is the
|
||||
// flaw the server-assembled windowed read model deletes:
|
||||
//
|
||||
// 1. Cold channel load paints the newest CHANNEL_HISTORY_LIMIT (60) rows.
|
||||
// 2. One of those rows is a reply whose root/parent is OUTSIDE that window.
|
||||
// 3. useLoadMissingAncestors fetches that old root by id and merges it into
|
||||
// the SAME channel cache — a non-contiguous "island" days older than the
|
||||
// contiguous window.
|
||||
// 4. pageOlderMessages anchors `oldestTimestamp = baseline[0].created_at` on
|
||||
// the island, so every scroll-up pages backward FROM the island and
|
||||
// permanently skips the real history between it and the true frontier.
|
||||
// 5. CLI `/query` returns those `gap-*` rows; the GUI never requests them.
|
||||
//
|
||||
// Contract: every top-level `gap-*` row the relay holds must be reachable in the
|
||||
// timeline. RED on main (the pager anchors on the island and skips the gap
|
||||
// entirely — `gap` rows reached === 0); GREEN on the windowed read model, whose
|
||||
// relay-owned cursor cannot be moved by an out-of-band ancestor merge.
|
||||
|
||||
const RELAY_HTTP = process.env.BUZZ_E2E_RELAY_URL ?? "http://localhost:3000";
|
||||
|
||||
// uuid5(NAMESPACE_DNS, "buzz.channel.general") — the seeded `general` channel.
|
||||
const GENERAL_CHANNEL_ID = "9f28288a-d724-587a-9709-92dc7f967110";
|
||||
|
||||
// >60 newest rows so the cold window (CHANNEL_HISTORY_LIMIT=60) does NOT reach
|
||||
// the gap; the gap sits below the frontier, the old root below the gap.
|
||||
const GAP_COUNT = 100;
|
||||
const NEWEST_COUNT = 70;
|
||||
|
||||
test.beforeAll(async () => {
|
||||
test.setTimeout(90_000);
|
||||
await assertRelaySeeded();
|
||||
});
|
||||
|
||||
test("live relay: an ancestor island does not strand the history frontier", async ({
|
||||
page,
|
||||
}, testInfo) => {
|
||||
testInfo.setTimeout(180_000);
|
||||
|
||||
// Per-run isolation: the suite seeds into shared `general`, which accumulates
|
||||
// rows across runs. A generic `gap \d+` match lets a prior run's rows inflate
|
||||
// the reachable set and false-green the parity assertion (observed: a
|
||||
// contaminated relay "passed" in 3.3s while a clean channel is RED with
|
||||
// seen.size === 0). Tag this run's rows and assert only against them.
|
||||
const nonce = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
|
||||
const scenario = ancestorIsland({
|
||||
channelId: GENERAL_CHANNEL_ID,
|
||||
gapCount: GAP_COUNT,
|
||||
newestCount: NEWEST_COUNT,
|
||||
nonce,
|
||||
});
|
||||
const expected = await seedScenario(scenario, { relayHttpUrl: RELAY_HTTP });
|
||||
const expectedGap = new Set(expected.map((e) => e.content));
|
||||
expect(expectedGap.size).toBe(GAP_COUNT);
|
||||
|
||||
await installRelayBridge(page, "tyler");
|
||||
await page.goto("/");
|
||||
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();
|
||||
|
||||
// Establish the poison precondition on the diseased client: on main,
|
||||
// useLoadMissingAncestors fetches the old root by id and merges it into the
|
||||
// channel cache, and "island root" appears in the timeline — that injected
|
||||
// island is what strands the pager. The overhaul DELETES useLoadMissingAncestors
|
||||
// (the relay-owned window cursor can't be moved by an out-of-band merge), so
|
||||
// the island is never injected and this row never appears. Best-effort, not a
|
||||
// hard gate: on main it settles the poison before we scroll; on the overhaul
|
||||
// it just times out harmlessly and we proceed to the reachability invariant —
|
||||
// which is the assertion that actually flips 0/100 (stranded) → 100/100.
|
||||
await timeline
|
||||
.getByText(`island root ${nonce}`, { exact: false })
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: 15_000 })
|
||||
.catch(() => {
|
||||
/* cured client: no ancestor fetch, no island — expected on the overhaul */
|
||||
});
|
||||
|
||||
// Union of THIS run's `gap` contents ever rendered. Virtualization only
|
||||
// mounts a window, so accumulate across scroll passes rather than snapshot
|
||||
// once. Match on the run nonce so a prior run's rows can't inflate the set.
|
||||
const gapPattern = new RegExp(`gap ${nonce} \\d+`);
|
||||
const renderedGapContents = async () =>
|
||||
timeline.evaluate((element, pattern: string) => {
|
||||
const re = new RegExp(pattern);
|
||||
const found: string[] = [];
|
||||
for (const row of (
|
||||
element as HTMLDivElement
|
||||
).querySelectorAll<HTMLElement>("[data-message-id]")) {
|
||||
const match = row.textContent?.match(re);
|
||||
if (match) found.push(match[0]);
|
||||
}
|
||||
return found;
|
||||
}, gapPattern.source);
|
||||
|
||||
// A real wheel-up gesture per pass: the older-history sentinel arms on a
|
||||
// genuine leave→enter transition (IntersectionObserver), so a raw scrollTop=0
|
||||
// write can fail to re-fire. A wheel event is what a real user issues.
|
||||
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<string>();
|
||||
const collect = async () => {
|
||||
for (const content of await renderedGapContents()) seen.add(content);
|
||||
};
|
||||
|
||||
await timeline.hover();
|
||||
let stallStreak = 0;
|
||||
for (let attempt = 0; attempt < 200 && seen.size < GAP_COUNT; attempt += 1) {
|
||||
const before = seen.size;
|
||||
await wheelToTop();
|
||||
try {
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
await collect();
|
||||
return seen.size;
|
||||
},
|
||||
{ timeout: 4_000 },
|
||||
)
|
||||
.toBeGreaterThan(before);
|
||||
} catch {
|
||||
// No growth this pass — count toward a genuine stall.
|
||||
}
|
||||
await collect();
|
||||
if (seen.size > before) {
|
||||
stallStreak = 0;
|
||||
} else {
|
||||
stallStreak += 1;
|
||||
if (stallStreak > 8) break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parity: every gap row the relay holds must be reachable. RED on main — the
|
||||
// pager anchors on the injected island (old root) and pages backward from it,
|
||||
// skipping the entire gap span (reaches ~0 of GAP_COUNT). GREEN on the
|
||||
// windowed read model, whose relay-owned cursor an ancestor merge can't move.
|
||||
expect(seen.size).toBeGreaterThan(GAP_COUNT * 0.9);
|
||||
});
|
||||
Reference in New Issue
Block a user