Skip to content

Commit f2aa42e

Browse files
loganjclaude
andcommitted
fix(desktop): preserve workstream PR loading state
Signed-off-by: loganj <loganj@squareup.com> Co-authored-by: Claude Code <noreply@anthropic.com> Signed-off-by: loganj <loganj@squareup.com>
1 parent fe8f657 commit f2aa42e

3 files changed

Lines changed: 167 additions & 23 deletions

File tree

desktop/src/features/workstream-board/lib/workstreamPullRequestStatus.test.mjs

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
11
import assert from "node:assert/strict";
2-
import test from "node:test";
2+
import { after, before, test } from "node:test";
3+
import { JSDOM } from "jsdom";
4+
5+
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
6+
url: "http://localhost",
7+
});
8+
9+
before(() => {
10+
Object.assign(globalThis, {
11+
document: dom.window.document,
12+
HTMLElement: dom.window.HTMLElement,
13+
IS_REACT_ACT_ENVIRONMENT: true,
14+
window: dom.window,
15+
});
16+
});
17+
18+
after(() => dom.window.close());
319

420
import {
521
createWorkstreamPollingRegistry,
@@ -155,3 +171,101 @@ test("polling deduplicates canonical identities and tears down", async () => {
155171
assert.equal(aborted, true);
156172
resolveRequest();
157173
});
174+
175+
test("workstream status hook exposes loading before a supported PR resolves", async () => {
176+
const { cleanup, renderHook } = await import("@testing-library/react");
177+
const { useWorkstreamPullRequestStatuses } = await import(
178+
"./workstreamPullRequestStatus.ts"
179+
);
180+
const reference = parseWorkstreamPullRequestReference(
181+
"https://github.com/block/buzz/pull/42",
182+
);
183+
const registry = createWorkstreamPollingRegistry({
184+
fetcher: () => new Promise(() => {}),
185+
});
186+
const references = [reference];
187+
const { result, unmount } = renderHook(() =>
188+
useWorkstreamPullRequestStatuses(references, registry),
189+
);
190+
assert.deepEqual(result.current.get(reference.identity), {
191+
status: "loading",
192+
});
193+
unmount();
194+
cleanup();
195+
});
196+
197+
test("workstream status hook preserves a synchronous live callback", async () => {
198+
const { act, cleanup, renderHook } = await import("@testing-library/react");
199+
const { useWorkstreamPullRequestStatuses } = await import(
200+
"./workstreamPullRequestStatus.ts"
201+
);
202+
const reference = parseWorkstreamPullRequestReference(
203+
"https://github.com/block/buzz/pull/42",
204+
);
205+
const registry = createWorkstreamPollingRegistry({
206+
fetcher: async (url) =>
207+
String(url).endsWith("/reviews?per_page=100")
208+
? response([])
209+
: response({ number: 42, title: "Board", state: "open", draft: false }),
210+
maxPolls: 1,
211+
});
212+
const prime = registry.subscribe(reference, () => {});
213+
await act(async () => {
214+
await Promise.resolve();
215+
await Promise.resolve();
216+
});
217+
const references = [reference];
218+
const { result, unmount } = renderHook(() =>
219+
useWorkstreamPullRequestStatuses(references, registry),
220+
);
221+
await act(async () => {
222+
await Promise.resolve();
223+
});
224+
assert.deepEqual(result.current.get(reference.identity), {
225+
status: "live",
226+
provider: "github",
227+
href: "https://github.com/block/buzz/pull/42",
228+
repository: "block/buzz",
229+
number: 42,
230+
title: "Board",
231+
lifecycle: "open",
232+
reviewState: "no-reviews",
233+
});
234+
unmount();
235+
prime();
236+
cleanup();
237+
});
238+
239+
test("workstream status hook removes stale references and preserves unsupported state", async () => {
240+
const { act, cleanup, renderHook } = await import("@testing-library/react");
241+
const { useWorkstreamPullRequestStatuses } = await import(
242+
"./workstreamPullRequestStatus.ts"
243+
);
244+
const supported = parseWorkstreamPullRequestReference(
245+
"https://github.com/block/buzz/pull/42",
246+
);
247+
const unsupported = parseWorkstreamPullRequestReference(
248+
"https://example.com/pr/1",
249+
);
250+
const registry = createWorkstreamPollingRegistry({
251+
fetcher: () => new Promise(() => {}),
252+
});
253+
const { result, rerender, unmount } = renderHook(
254+
({ refs }) => useWorkstreamPullRequestStatuses(refs, registry),
255+
{ initialProps: { refs: [supported, unsupported] } },
256+
);
257+
assert.deepEqual(
258+
result.current.get(unsupported.identity)?.status,
259+
"unsupported",
260+
);
261+
await act(async () => {
262+
rerender({ refs: [unsupported] });
263+
});
264+
assert.equal(result.current.has(supported.identity), false);
265+
assert.deepEqual(
266+
result.current.get(unsupported.identity)?.status,
267+
"unsupported",
268+
);
269+
unmount();
270+
cleanup();
271+
});

desktop/src/features/workstream-board/lib/workstreamPullRequestStatus.ts

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -507,8 +507,14 @@ export function createWorkstreamPollingRegistry(
507507

508508
const defaultWorkstreamPollingRegistry = createWorkstreamPollingRegistry();
509509

510+
type WorkstreamPollingRegistry = Pick<
511+
ReturnType<typeof createWorkstreamPollingRegistry>,
512+
"subscribe"
513+
>;
514+
510515
export function useWorkstreamPullRequestStatuses(
511516
references: readonly WorkstreamPullRequestReference[],
517+
pollingRegistry: WorkstreamPollingRegistry = defaultWorkstreamPollingRegistry,
512518
): ReadonlyMap<string, WorkstreamPullRequestDisplayState> {
513519
const [states, setStates] = React.useState<
514520
ReadonlyMap<string, WorkstreamPullRequestDisplayState>
@@ -520,34 +526,37 @@ export function useWorkstreamPullRequestStatuses(
520526
}, [references]);
521527

522528
React.useEffect(() => {
523-
const next = new Map<string, WorkstreamPullRequestDisplayState>();
529+
const initial = new Map<string, WorkstreamPullRequestDisplayState>();
530+
for (const reference of referencesByIdentity.values()) {
531+
initial.set(
532+
reference.identity,
533+
reference.kind === "unsupported"
534+
? {
535+
status: "unsupported",
536+
href: reference.href,
537+
reason: reference.reason,
538+
}
539+
: { status: "loading" },
540+
);
541+
}
542+
setStates(initial);
543+
524544
const unsubscribers: Array<() => void> = [];
525545
for (const reference of referencesByIdentity.values()) {
526-
if (reference.kind === "unsupported") {
527-
next.set(reference.identity, {
528-
status: "unsupported",
529-
href: reference.href,
530-
reason: reference.reason,
546+
if (reference.kind === "unsupported") continue;
547+
const unsubscribe = pollingRegistry.subscribe(reference, (state) => {
548+
setStates((current) => {
549+
const updated = new Map(current);
550+
updated.set(reference.identity, state);
551+
return updated;
531552
});
532-
continue;
533-
}
534-
const unsubscribe = defaultWorkstreamPollingRegistry.subscribe(
535-
reference,
536-
(state) => {
537-
setStates((current) => {
538-
const updated = new Map(current);
539-
updated.set(reference.identity, state);
540-
return updated;
541-
});
542-
},
543-
);
553+
});
544554
unsubscribers.push(unsubscribe);
545555
}
546-
setStates(next);
547556
return () => {
548557
for (const unsubscribe of unsubscribers) unsubscribe();
549558
};
550-
}, [referencesByIdentity]);
559+
}, [pollingRegistry, referencesByIdentity]);
551560

552561
return states;
553562
}

desktop/tests/e2e/workstream-board-b3-pr.capture.spec.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,34 @@ test("continuous relay-backed Workstream Board bullet 3 PR-status demo", async (
99
await installRelayBridge(page, "tyler", { seedPreviewFeatures: true });
1010
await page.routeWebSocket(/localhost:3000/, (socket) => {
1111
const server = socket.connectToServer();
12+
let delayedPrSubscription: string | null = null;
13+
let delayedFrames: string[] = [];
14+
let releaseScheduled = false;
15+
const release = () => {
16+
const frames = delayedFrames;
17+
delayedFrames = [];
18+
delayedPrSubscription = null;
19+
releaseScheduled = false;
20+
for (const frame of frames) socket.send(frame);
21+
};
1222
server.onMessage((message) => {
1323
if (typeof message === "string") {
1424
try {
1525
const payload = JSON.parse(message) as unknown[];
26+
const type = payload[0];
27+
const subId = typeof payload[1] === "string" ? payload[1] : null;
1628
const event = payload[2] as { kind?: unknown } | undefined;
17-
if (payload[0] === "EVENT" && event?.kind === 1618) {
18-
setTimeout(() => socket.send(message), 2_500);
29+
if (type === "EVENT" && event?.kind === 1618 && subId) {
30+
delayedPrSubscription = subId;
31+
delayedFrames.push(message);
32+
if (!releaseScheduled) {
33+
releaseScheduled = true;
34+
setTimeout(release, 2_500);
35+
}
36+
return;
37+
}
38+
if (type === "EOSE" && subId === delayedPrSubscription) {
39+
delayedFrames.push(message);
1940
return;
2041
}
2142
} catch {

0 commit comments

Comments
 (0)