diff --git a/canvas/src/components/__tests__/CommunicationOverlay.test.tsx b/canvas/src/components/__tests__/CommunicationOverlay.test.tsx index 1612f8eb..3bed0076 100644 --- a/canvas/src/components/__tests__/CommunicationOverlay.test.tsx +++ b/canvas/src/components/__tests__/CommunicationOverlay.test.tsx @@ -99,7 +99,7 @@ describe("CommunicationOverlay — fan-out cap", () => { }); }); -describe("CommunicationOverlay — visibility gate", () => { +describe("CommunicationOverlay — cadence", () => { it("uses 30s interval cadence (was 10s pre-fix)", async () => { await act(async () => { render(); @@ -119,3 +119,60 @@ describe("CommunicationOverlay — visibility gate", () => { expect(mockGet).toHaveBeenCalledTimes(6); // +3 from second tick }); }); + +describe("CommunicationOverlay — visibility gate", () => { + // The visibility gate is the dial that drops collapsed-panel polling + // to ZERO. The cadence test above can't catch its removal — if a + // refactor dropped `if (!visible) return`, the cadence test would + // still pass because the effect would still fire every 30s. + // + // Direct probe: render with comms-returning mock so the panel + // actually renders (close button only exists in the expanded panel, + // not the collapsed button-state). Click close, advance the clock, + // assert no further fetches. + it("stops polling after the user collapses the panel", async () => { + // Mock returns one a2a_send so comms.length > 0 → panel renders → + // close button accessible. + mockGet.mockResolvedValue([ + { + id: "act-1", + workspace_id: "ws-1", + activity_type: "a2a_send", + source_id: "ws-1", + target_id: "ws-2", + summary: "test", + status: "completed", + duration_ms: 100, + created_at: new Date().toISOString(), + }, + ]); + + const { getByLabelText } = await act(async () => { + return render(); + }); + // Drain pending microtasks (resolves the await in fetchComms) so + // setComms lands and the panel renders. Don't advance time — that + // would fire the next interval tick and pollute the assertion. + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + }); + // Initial mount polled 3 workspaces. + expect(mockGet).toHaveBeenCalledTimes(3); + mockGet.mockClear(); + + // Click the close button. Synchronous getByLabelText avoids + // findBy's internal setTimeout (deadlocks under useFakeTimers). + const closeBtn = getByLabelText("Close communications panel"); + await act(async () => { + fireEvent.click(closeBtn); + }); + + // Advance well past the 30s cadence — gate should suppress the tick. + await act(async () => { + vi.advanceTimersByTime(60_000); + }); + expect(mockGet).not.toHaveBeenCalled(); + }); +});