From f34b8ef70f7af0888c9fe88aa809cf2716796ccc Mon Sep 17 00:00:00 2001 From: Neil Date: Wed, 24 Jun 2026 20:55:28 -0700 Subject: [PATCH] Cover sidebar worker log activation --- .../WorktreeCardAgents.activation.test.tsx | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx diff --git a/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx b/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx new file mode 100644 index 00000000000..919b4ac97cd --- /dev/null +++ b/src/renderer/src/components/sidebar/WorktreeCardAgents.activation.test.tsx @@ -0,0 +1,249 @@ +// @vitest-environment happy-dom + +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { renderToStaticMarkup } from 'react-dom/server' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { DashboardAgentRow as DashboardAgentRowData } from '@/components/dashboard/useDashboardData' +import { makePaneKey } from '../../../../shared/stable-pane-id' + +const LEAF_A = '11111111-1111-4111-8111-111111111111' +const LEAF_B = '22222222-2222-4222-8222-222222222222' + +type MockAgentOptions = { + paneKey: string + tabId: string + agentType: string + prompt: string + worktreeId: string + startedAt?: number +} + +function mockAgent({ + paneKey, + tabId, + agentType, + prompt, + worktreeId, + startedAt = 1000 +}: MockAgentOptions): DashboardAgentRowData { + return { + paneKey, + tab: { id: tabId }, + agentType, + rowSource: 'live', + state: 'working', + startedAt, + entry: { + prompt, + state: 'working', + paneKey, + updatedAt: startedAt, + stateStartedAt: startedAt, + stateHistory: [], + worktreeId + } + } as unknown as DashboardAgentRowData +} + +let mockAgents: DashboardAgentRowData[] = [] +let mockAgentActivityDisplayMode: 'compact' | 'full' | undefined +let mockTabsByWorktree: Record = {} +let mockAgentStatusByPaneKey: Record = {} +let capturedRowActivations: { + paneKey: string + onActivate: (tabId: string, paneKey: string) => void +}[] = [] + +function buildMockStoreState(): Record { + return { + agentActivityDisplayMode: mockAgentActivityDisplayMode, + acknowledgedAgentsByPaneKey: {}, + cacheTimerByKey: {}, + dropAgentStatus: vi.fn(), + dismissRetainedAgent: vi.fn(), + acknowledgeAgents: vi.fn(), + agentSendPopoverTargetMode: null, + agentStatusByPaneKey: mockAgentStatusByPaneKey, + agentStatusEpoch: 0, + tabsByWorktree: mockTabsByWorktree, + terminalLayoutsByTabId: {}, + ptyIdsByTabId: {}, + runtimePaneTitlesByTabId: {}, + sendPromptToSidebarAgentTarget: vi.fn(), + settings: { + promptCacheTimerEnabled: true, + promptCacheTtlMs: 60_000 + } + } +} + +const activationMocks = vi.hoisted(() => ({ + activateAndRevealWorktree: vi.fn(), + activateTabAndFocusPane: vi.fn() +})) + +const staleAgentRowMocks = vi.hoisted(() => ({ + dismissStaleAgentRowByKey: vi.fn() +})) + +vi.mock('@/store', () => ({ + useAppStore: Object.assign( + (selector: (state: unknown) => unknown) => selector(buildMockStoreState()), + { + getState: () => buildMockStoreState() + } + ) +})) + +vi.mock('@/lib/worktree-activation', () => ({ + activateAndRevealWorktree: activationMocks.activateAndRevealWorktree +})) + +vi.mock('@/lib/activate-tab-and-focus-pane', () => ({ + activateTabAndFocusPane: activationMocks.activateTabAndFocusPane +})) + +vi.mock('../terminal-pane/stale-agent-row', () => ({ + dismissStaleAgentRowByKey: staleAgentRowMocks.dismissStaleAgentRowByKey +})) + +vi.mock('./useWorktreeAgentRows', () => ({ + useWorktreeAgentRows: vi.fn(() => mockAgents) +})) + +vi.mock('@/components/dashboard/useNow', () => ({ + useNow: vi.fn(() => 2000) +})) + +vi.mock('@/components/dashboard/DashboardAgentRow', () => ({ + default: ({ + agent, + onActivate + }: { + agent: DashboardAgentRowData + onActivate: (tabId: string, paneKey: string) => void + }) => { + capturedRowActivations.push({ paneKey: agent.paneKey, onActivate }) + return
+ } +})) + +vi.mock('./focused-agent-row-highlight', () => ({ + useFocusedAgentPaneKey: vi.fn(() => null) +})) + +describe('WorktreeCardAgents activation', () => { + beforeEach(() => { + vi.clearAllMocks() + activationMocks.activateAndRevealWorktree.mockImplementation(() => undefined) + mockAgents = [] + mockAgentActivityDisplayMode = undefined + mockTabsByWorktree = {} + mockAgentStatusByPaneKey = {} + capturedRowActivations = [] + }) + + it('reveals the worktree and focuses an automation worker row hydrated during reveal', async () => { + mockAgentActivityDisplayMode = 'full' + const tabId = 'worker-tab' + const paneKey = makePaneKey(tabId, LEAF_A) + mockAgents = [ + mockAgent({ + paneKey, + tabId, + agentType: 'codex', + prompt: 'Run automation worker', + worktreeId: 'wt-1' + }) + ] + mockAgentStatusByPaneKey = { [paneKey]: { worktreeId: 'wt-1' } } + // Why: activation must use the post-reveal store snapshot, matching tab + // hydration that arrives while a background worker is being opened. + activationMocks.activateAndRevealWorktree.mockImplementation(() => { + mockTabsByWorktree = { 'wt-1': [{ id: tabId }] } + }) + const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents') + + renderToStaticMarkup() + expect(capturedRowActivations).toHaveLength(1) + capturedRowActivations[0].onActivate(tabId, paneKey) + + expect(activationMocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1') + expect(activationMocks.activateTabAndFocusPane).toHaveBeenCalledWith(tabId, LEAF_A, { + ackPaneKeyOnSuccess: paneKey, + flashFocusedPane: true, + scrollToBottomIfOutputSinceLastView: true + }) + expect(staleAgentRowMocks.dismissStaleAgentRowByKey).not.toHaveBeenCalled() + }) + + it('keeps a live worktree-attributed row visible while its tab is hydrating', async () => { + mockAgentActivityDisplayMode = 'full' + const tabId = 'hydrating-worker-tab' + const paneKey = makePaneKey(tabId, LEAF_B) + mockAgents = [ + mockAgent({ + paneKey, + tabId, + agentType: 'claude', + prompt: 'Hydrating worker', + worktreeId: 'wt-1' + }) + ] + mockAgentStatusByPaneKey = { [paneKey]: { worktreeId: 'wt-1' } } + const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents') + + renderToStaticMarkup() + expect(capturedRowActivations).toHaveLength(1) + capturedRowActivations[0].onActivate(tabId, paneKey) + + expect(activationMocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1') + expect(activationMocks.activateTabAndFocusPane).not.toHaveBeenCalled() + expect(staleAgentRowMocks.dismissStaleAgentRowByKey).not.toHaveBeenCalled() + }) + + it('reveals the worktree and focuses a compact automation worker row hydrated during reveal', async () => { + mockAgentActivityDisplayMode = 'compact' + const tabId = 'compact-worker-tab' + const paneKey = makePaneKey(tabId, LEAF_A) + mockAgents = [ + mockAgent({ + paneKey, + tabId, + agentType: 'gemini', + prompt: 'Compact worker', + worktreeId: 'wt-1' + }) + ] + mockAgentStatusByPaneKey = { [paneKey]: { worktreeId: 'wt-1' } } + // Why: compact rows share the same activation contract as full rows, so + // this keeps the test pinned to reveal-time tab hydration. + activationMocks.activateAndRevealWorktree.mockImplementation(() => { + mockTabsByWorktree = { 'wt-1': [{ id: tabId }] } + }) + const host = document.createElement('div') + document.body.append(host) + const root: Root = createRoot(host) + const { default: WorktreeCardAgents } = await import('./WorktreeCardAgents') + + await act(async () => { + root.render() + }) + const row = host.querySelector('.compact-agent-row') + expect(row).toBeInstanceOf(HTMLElement) + + await act(async () => { + row?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(activationMocks.activateAndRevealWorktree).toHaveBeenCalledWith('wt-1') + expect(activationMocks.activateTabAndFocusPane).toHaveBeenCalledWith(tabId, LEAF_A, { + ackPaneKeyOnSuccess: paneKey, + flashFocusedPane: true, + scrollToBottomIfOutputSinceLastView: true + }) + act(() => root.unmount()) + host.remove() + }) +})