test(desktop): latch observer archive policy

Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
This commit is contained in:
npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta
2026-08-02 13:18:31 -04:00
parent 6eeeb3b8de
commit 8614e6f71c
3 changed files with 50 additions and 6 deletions

View File

@@ -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<string>;
};
let deferredGetEventQueue: DeferredGetEvent[] = [];
let deferredObserverArchivePolicyQueue: Array<() => void> = [];
const mockDisplayNames = new Map<string, string>([
[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<void>((resolve) => {
deferredObserverArchivePolicyQueue.push(resolve);
window.__BUZZ_E2E_OBSERVER_ARCHIVE_POLICY_PENDING__ =
deferredObserverArchivePolicyQueue.length;
});
}
const delayMs =
activeConfig?.mock?.observerArchiveDefaultEnabledDelayMs;
if (delayMs && delayMs > 0) {

View File

@@ -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(

View File

@@ -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.