Files
buzz/desktop/src/features/pulse/focusRefetchPolicy.test.mjs
T
59f613c404 fix(desktop): defer foreground resume work (#5696)
## Summary

- defer foreground resume work until the activation task has returned, a
frame has painted, and a trailing task gets a turn
- centralize app-focus subscribers and remove the broad TanStack
`refetchOnWindowFocus` fan-out
- coalesce relay recovery and preserve an explicit deferred refresh only
for workflow data without a polling/push freshness path
- defer the notification permission native check while keeping blur and
cheap correctness signals immediate

## Why

Buzz Desktop 0.5.10 can spend roughly 1.5 seconds in the WebKit
window-focus listener/microtask checkpoint before returning to the run
loop. Focus currently fans out into query refetches, React polling
updates, relay reconnect/replay, and native work in one activation turn.
This patch establishes an interaction-first foreground boundary rather
than letting those consumers compete with the activating input and first
paint.

## Validation

- focused foreground/workflow/relay tests: 18/18 passed before commit
- `pnpm --dir desktop typecheck`: passed before commit
- pre-commit desktop check and file-size gate: passed
- pre-push desktop check, typecheck, and full desktop unit suite:
4,743/4,743 passed at `704e7b4b6618fafce655bb2b07c7a9fe0fc8c643`
- Princess Donut independent adversarial review: PASS after two
lifecycle/freshness blockers were resolved

## Manual test

1. Install the PR build and use Buzz long enough to populate channels,
home, workflows, agents, and other polling surfaces.
2. Switch to another app for 30-60 seconds.
3. Return by clicking Buzz and immediately click a channel or scroll.
4. Confirm the first interaction and paint are prompt, then confirm
channels/home/workflows refresh and a degraded relay reconnects after
the activation boundary.
5. Repeat while rapidly switching away again to verify no resume work
starts after focus has been lost.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
2026-08-12 13:46:49 -07:00

66 lines
1.5 KiB
JavaScript

import assert from "node:assert/strict";
import { afterEach, test } from "node:test";
import {
focusManager,
QueryClient,
QueryObserver,
} from "@tanstack/react-query";
import { pulseFocusRefetchPolicy } from "./hooks.ts";
afterEach(() => {
focusManager.setFocused(undefined);
});
async function focusRefetchCount({ ageMs, policy }) {
focusManager.setFocused(false);
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
queryClient.mount();
const queryKey = ["focus-refetch-policy", policy.staleTime, ageMs];
queryClient.setQueryData(queryKey, "cached", {
updatedAt: Date.now() - ageMs,
});
let fetchCount = 0;
const observer = new QueryObserver(queryClient, {
queryKey,
queryFn: async () => {
fetchCount += 1;
return "refetched";
},
refetchOnMount: false,
...policy,
});
const unsubscribe = observer.subscribe(() => {});
focusManager.setFocused(true);
await new Promise((resolve) => setImmediate(resolve));
unsubscribe();
queryClient.unmount();
return fetchCount;
}
test("pulse: skips fresh focus refetch", async () => {
assert.equal(
await focusRefetchCount({
ageMs: pulseFocusRefetchPolicy.staleTime - 1_000,
policy: pulseFocusRefetchPolicy,
}),
0,
);
});
test("pulse: does not refetch stale data on focus", async () => {
assert.equal(
await focusRefetchCount({
ageMs: pulseFocusRefetchPolicy.staleTime + 1,
policy: pulseFocusRefetchPolicy,
}),
0,
);
});