diff --git a/ui/src/context/LiveUpdatesProvider.test.ts b/ui/src/context/LiveUpdatesProvider.test.ts index 559f80df1e9..96c5d3b98c6 100644 --- a/ui/src/context/LiveUpdatesProvider.test.ts +++ b/ui/src/context/LiveUpdatesProvider.test.ts @@ -318,6 +318,108 @@ describe("LiveUpdatesProvider issue invalidation", () => { refetchType: "inactive", }); }); + + it("refreshes visible issue run queries when the displayed run changes status", () => { + const invalidations: unknown[] = []; + const queryClient = { + invalidateQueries: (input: unknown) => { + invalidations.push(input); + }, + getQueryData: (key: unknown) => { + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.detail("PAP-759"))) { + return { + id: "issue-1", + identifier: "PAP-759", + assigneeAgentId: "agent-1", + }; + } + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.activeRun("PAP-759"))) { + return { + id: "run-1", + }; + } + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.liveRuns("PAP-759"))) { + return [{ id: "run-1" }]; + } + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.runs("PAP-759"))) { + return [{ runId: "run-1" }]; + } + return undefined; + }, + }; + + const invalidated = __liveUpdatesTestUtils.invalidateVisibleIssueRunQueries( + queryClient as never, + "/PAP/issues/PAP-759", + { + runId: "run-1", + agentId: "agent-1", + status: "succeeded", + }, + { isForegrounded: true }, + ); + + expect(invalidated).toBe(true); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.detail("PAP-759"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.activity("PAP-759"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.runs("PAP-759"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.liveRuns("PAP-759"), + }); + expect(invalidations).toContainEqual({ + queryKey: queryKeys.issues.activeRun("PAP-759"), + }); + }); + + it("ignores run status events for other issues", () => { + const invalidations: unknown[] = []; + const queryClient = { + invalidateQueries: (input: unknown) => { + invalidations.push(input); + }, + getQueryData: (key: unknown) => { + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.detail("PAP-759"))) { + return { + id: "issue-1", + identifier: "PAP-759", + assigneeAgentId: "agent-1", + }; + } + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.activeRun("PAP-759"))) { + return { + id: "run-1", + }; + } + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.liveRuns("PAP-759"))) { + return [{ id: "run-1" }]; + } + if (JSON.stringify(key) === JSON.stringify(queryKeys.issues.runs("PAP-759"))) { + return [{ runId: "run-1" }]; + } + return undefined; + }, + }; + + const invalidated = __liveUpdatesTestUtils.invalidateVisibleIssueRunQueries( + queryClient as never, + "/PAP/issues/PAP-759", + { + runId: "run-2", + agentId: "agent-2", + status: "succeeded", + }, + { isForegrounded: true }, + ); + + expect(invalidated).toBe(false); + expect(invalidations).toEqual([]); + }); }); describe("LiveUpdatesProvider visible issue comment hydration", () => { diff --git a/ui/src/context/LiveUpdatesProvider.tsx b/ui/src/context/LiveUpdatesProvider.tsx index 1db342c0405..a146ca66f01 100644 --- a/ui/src/context/LiveUpdatesProvider.tsx +++ b/ui/src/context/LiveUpdatesProvider.tsx @@ -245,6 +245,30 @@ function shouldSuppressRunStatusToastForVisibleIssue( return !!agentId && !!context.assigneeAgentId && agentId === context.assigneeAgentId; } +function invalidateVisibleIssueRunQueries( + queryClient: QueryClient, + pathname: string, + payload: Record, + options?: VisibleRouteOptions, +): boolean { + const context = resolveVisibleIssueRouteContext(queryClient, pathname, options); + if (!context) return false; + + const runId = readString(payload.runId); + const agentId = readString(payload.agentId); + const matchesVisibleIssue = + (runId !== null && context.runIds.has(runId)) || + (!!agentId && !!context.assigneeAgentId && agentId === context.assigneeAgentId); + if (!matchesVisibleIssue) return false; + + queryClient.invalidateQueries({ queryKey: queryKeys.issues.detail(context.routeIssueRef) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.activity(context.routeIssueRef) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.runs(context.routeIssueRef) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.liveRuns(context.routeIssueRef) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.activeRun(context.routeIssueRef) }); + return true; +} + function shouldSuppressAgentStatusToastForVisibleIssue( queryClient: QueryClient, pathname: string, @@ -735,6 +759,7 @@ function handleLiveEvent( if (event.type === "heartbeat.run.queued" || event.type === "heartbeat.run.status") { invalidateHeartbeatQueries(queryClient, expectedCompanyId, payload); + invalidateVisibleIssueRunQueries(queryClient, pathname, payload); if (event.type === "heartbeat.run.status") { const toast = buildRunStatusToast(payload, nameOf); if ( @@ -830,6 +855,7 @@ export const __liveUpdatesTestUtils = { closeSocketQuietly, hydrateVisibleIssueComment, invalidateActivityQueries, + invalidateVisibleIssueRunQueries, resolveLiveCompanyId, shouldDeferIssueRefetchForVisibleAgentActivity, shouldDeferVisibleIssueCommentActivity,