From 8614e6f71c690185d497d47f2ad493f59da0dad9 Mon Sep 17 00:00:00 2001 From: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta Date: Sun, 2 Aug 2026 13:18:31 -0400 Subject: [PATCH] test(desktop): latch observer archive policy Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta --- desktop/src/testing/e2eBridge.ts | 27 ++++++++++++++++--- .../tests/e2e/observer-archive-policy.spec.ts | 20 +++++++++++++- desktop/tests/helpers/bridge.ts | 9 +++++-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 355ccea9fc..0d9899c7f8 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -387,11 +387,12 @@ type E2eConfig = { observerArchiveDefaultEnabled?: boolean; /** * Delay (ms) applied to `observer_archive_default_enabled` so E2E tests - * can observe the pending-reconciliation state (toggle disabled, no - * archive-manager `list_save_subscriptions` call) before the policy - * resolves. 0/undefined = instant. + * can exercise short-lived loading UI. 0/undefined = instant. Prefer the + * explicit defer/release seam for pending-state ordering assertions. */ observerArchiveDefaultEnabledDelayMs?: number; + /** Hold the observer policy command until the E2E release seam is called. */ + deferObserverArchiveDefaultEnabled?: boolean; /** * When set, `observer_archive_default_enabled` throws with this message * instead of resolving — drives the fail-closed `.catch()` path in @@ -1249,6 +1250,10 @@ declare global { /** Count of `get_event` invocations for the current defer-target ID since * the last time `__BUZZ_E2E_DEFER_GET_EVENT__` was set. */ __BUZZ_E2E_GET_EVENT_CALL_COUNT__?: number; + /** Release every deferred observer archive policy command. */ + __BUZZ_E2E_RELEASE_OBSERVER_ARCHIVE_POLICY__?: () => number; + /** Number of observer archive policy commands currently held by the seam. */ + __BUZZ_E2E_OBSERVER_ARCHIVE_POLICY_PENDING__?: number; } } @@ -1352,6 +1357,7 @@ type DeferredGetEvent = { run: () => Promise; }; let deferredGetEventQueue: DeferredGetEvent[] = []; +let deferredObserverArchivePolicyQueue: Array<() => void> = []; const mockDisplayNames = new Map([ [MOCK_IDENTITY_PUBKEY, DEFAULT_MOCK_IDENTITY.display_name], @@ -9769,6 +9775,14 @@ export function maybeInstallE2eTauriMocks() { window.__BUZZ_E2E_GET_EVENT_CALL_COUNT__ = 0; window.__BUZZ_E2E_DEFER_GET_EVENT__ = null; deferredGetEventQueue = []; + deferredObserverArchivePolicyQueue = []; + window.__BUZZ_E2E_OBSERVER_ARCHIVE_POLICY_PENDING__ = 0; + window.__BUZZ_E2E_RELEASE_OBSERVER_ARCHIVE_POLICY__ = () => { + const queued = deferredObserverArchivePolicyQueue.splice(0); + window.__BUZZ_E2E_OBSERVER_ARCHIVE_POLICY_PENDING__ = 0; + for (const resolve of queued) resolve(); + return queued.length; + }; window.__BUZZ_E2E_RELEASE_GET_EVENT__ = () => { const queued = deferredGetEventQueue.splice(0); for (const entry of queued) { @@ -12130,6 +12144,13 @@ export function maybeInstallE2eTauriMocks() { // Returns the ArchiveBatchResult shape the UI expects. return { persisted: 0, dropped: 0 }; case "observer_archive_default_enabled": { + if (activeConfig?.mock?.deferObserverArchiveDefaultEnabled) { + await new Promise((resolve) => { + deferredObserverArchivePolicyQueue.push(resolve); + window.__BUZZ_E2E_OBSERVER_ARCHIVE_POLICY_PENDING__ = + deferredObserverArchivePolicyQueue.length; + }); + } const delayMs = activeConfig?.mock?.observerArchiveDefaultEnabledDelayMs; if (delayMs && delayMs > 0) { diff --git a/desktop/tests/e2e/observer-archive-policy.spec.ts b/desktop/tests/e2e/observer-archive-policy.spec.ts index 44a518e3cf..de297df5b6 100644 --- a/desktop/tests/e2e/observer-archive-policy.spec.ts +++ b/desktop/tests/e2e/observer-archive-policy.spec.ts @@ -226,7 +226,7 @@ test.describe("observer archive policy — reconciliation gate", () => { }) => { await installMockBridge(page, { observerArchiveDefaultEnabled: true, - observerArchiveDefaultEnabledDelayMs: 500, + deferObserverArchiveDefaultEnabled: true, saveSubscriptions: [ { scope_type: "owner_p", @@ -237,6 +237,14 @@ test.describe("observer archive policy — reconciliation gate", () => { }); await page.goto("/", { waitUntil: "domcontentloaded" }); + await page.waitForFunction( + () => + ( + window as Window & { + __BUZZ_E2E_OBSERVER_ARCHIVE_POLICY_PENDING__?: number; + } + ).__BUZZ_E2E_OBSERVER_ARCHIVE_POLICY_PENDING__ === 1, + ); await expect(page.getByTestId("channel-general")).toBeVisible({ timeout: 10_000, }); @@ -272,6 +280,16 @@ test.describe("observer archive policy — reconciliation gate", () => { ); expect(hasSubscriptionWhilePending).toBe(false); + const released = await page.evaluate( + () => + ( + window as Window & { + __BUZZ_E2E_RELEASE_OBSERVER_ARCHIVE_POLICY__?: () => number; + } + ).__BUZZ_E2E_RELEASE_OBSERVER_ARCHIVE_POLICY__?.() ?? 0, + ); + expect(released).toBe(1); + // After the policy resolves, both the IPC call and the live filter // appear. await page.waitForFunction( diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 8a9ab2be11..7e8192dd9f 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -322,10 +322,15 @@ type MockBridgeOptions = { observerArchiveDefaultEnabled?: boolean; /** * Delay (ms) applied to `observer_archive_default_enabled` so specs can - * assert the pending-reconciliation state (toggle disabled, no - * `list_save_subscriptions` call yet) before the policy resolves. + * exercise short-lived loading UI. Prefer the explicit defer/release seam + * when asserting behavior while the policy check is pending. */ observerArchiveDefaultEnabledDelayMs?: number; + /** + * Hold `observer_archive_default_enabled` until the test calls + * `__BUZZ_E2E_RELEASE_OBSERVER_ARCHIVE_POLICY__`. + */ + deferObserverArchiveDefaultEnabled?: boolean; /** * When set, `observer_archive_default_enabled` throws with this message — * drives the fail-closed path when the policy check itself fails.