diff --git a/packages/web/src/components/layout/Sidebar.tsx b/packages/web/src/components/layout/Sidebar.tsx index 7a9ba55..97c9d62 100644 --- a/packages/web/src/components/layout/Sidebar.tsx +++ b/packages/web/src/components/layout/Sidebar.tsx @@ -60,7 +60,9 @@ function useLastDataUpdate(): string { useEffect(() => { function refresh() { - setNow(Date.now()); + // No setNow here — calling setNow on every cache event causes a render loop on + // CI (each Date.now() call crosses a ms boundary → new value → React re-renders + // Sidebar → cache events fire again → loop). setNow belongs only in the interval. const latest = queryClient .getQueryCache() .getAll() @@ -70,7 +72,10 @@ function useLastDataUpdate(): string { refresh(); const unsubscribe = queryClient.getQueryCache().subscribe(refresh); - const interval = window.setInterval(refresh, 30_000); + const interval = window.setInterval(() => { + setNow(Date.now()); // refresh relative-time display ("X ago") every 30s + refresh(); + }, 30_000); return () => { unsubscribe(); window.clearInterval(interval); diff --git a/packages/web/src/test/dashboard.test.tsx b/packages/web/src/test/dashboard.test.tsx index b11c3b8..b97ce83 100644 --- a/packages/web/src/test/dashboard.test.tsx +++ b/packages/web/src/test/dashboard.test.tsx @@ -59,6 +59,22 @@ function renderDashboard() { describe("Dashboard — unified server-aware view", () => { afterEach(() => localStorage.clear()); + it("does not loop when Date.now advances on each call (CI render-loop repro)", async () => { + // On CI, consecutive Date.now() calls cross millisecond boundaries, so setNow(Date.now()) + // in the cache-event subscriber always produces a new value → React keeps re-rendering + // Sidebar → hits the 25-cycle "Maximum update depth exceeded" limit. + // This test forces that CI condition locally to catch regressions. + let t = 1_000_000; + const spy = vi.spyOn(Date, "now").mockImplementation(() => t++); + saveStore({ instances: [neo, iris], activeId: "neo" }); + renderDashboard(); + await waitFor(() => { + expect(screen.getByText("(Neo)")).toBeInTheDocument(); + expect(screen.getByText("(Iris)")).toBeInTheDocument(); + }); + spy.mockRestore(); + }); + it("lists each server's workspaces labelled with the server name", async () => { saveStore({ instances: [neo, iris], activeId: "neo" }); renderDashboard();