test(agent-status): cover needs-you glyph surfaces

This commit is contained in:
Brennan Benson
2026-07-22 15:49:00 -07:00
parent 1426a8c057
commit e9ef0a5b95
11 changed files with 59 additions and 34 deletions
@@ -51,12 +51,10 @@ describe('AgentStateDot', () => {
(state) => {
const markup = renderMarkup(state)
// Why: the "needs you" states share the dashboard's MessageCircleQuestion
// glyph rather than a bare dot. Assert the lucide class hook + amber text
// color without coupling to the exact SVG path lucide emits.
expect(markup).toContain('lucide-message-circle-question')
expect(markup).toContain('lucide-message-circle-question-mark')
expect(markup).toContain('text-amber-500')
expect(markup).not.toContain('bg-amber-500')
expect(markup).not.toContain('data-agent-spinner')
}
)
@@ -96,11 +96,6 @@ export const AgentStateDot = React.memo(function AgentStateDot({
}
if (state === 'permission' || state === 'waiting') {
// Why: the "needs you" states (an agent asking a question / awaiting
// input) render the same MessageCircleQuestion glyph the dashboard card
// uses for its pending-question pill, so "an agent needs you" reads
// identically across the board, sidebar rows, and terminal tabs instead
// of as an ambiguous amber dot. Amber still carries the attention color.
return (
<span
className={cn('inline-flex shrink-0 items-center justify-center', box, className)}
@@ -60,6 +60,30 @@ describe('AgentKanbanCard', () => {
expect(screen.queryByText(/\d+d/)).not.toBeInTheDocument()
})
it('shows the question glyph once when a summary is available', () => {
const attentionCard = card({
bucket: 'attention',
dotState: 'waiting',
askSummary: 'Approve deploy?'
})
const { container, rerender } = render(
<AgentKanbanCard card={attentionCard} now={2_000} onOpenTerminal={vi.fn()} />
)
expect(screen.queryByTestId('state-dot')).not.toBeInTheDocument()
expect(container.querySelectorAll('.lucide-message-circle-question-mark')).toHaveLength(1)
rerender(
<AgentKanbanCard
card={{ ...attentionCard, askSummary: undefined }}
now={2_000}
onOpenTerminal={vi.fn()}
/>
)
expect(screen.getByTestId('state-dot')).toBeInTheDocument()
expect(container.querySelector('.lucide-message-circle-question-mark')).toBeNull()
})
it('skips structured-clone rerenders until visible card data or its age changes', () => {
const onOpenTerminal = vi.fn()
const initial = card({ startedAt: 1_000 })
@@ -98,9 +98,7 @@ export const AgentKanbanCard = memo(
>
{card.worktreeName}
</span>
{/* Why: a pending question already renders the amber
MessageCircleQuestion glyph in the summary pill below, so suppress
the header state glyph here to avoid showing "needs you" twice. */}
{/* The summary pill already carries the attention glyph. */}
{card.askSummary ? null : <AgentStateDot state={card.dotState} className="ml-auto" />}
</div>
@@ -252,9 +252,7 @@ describe('DashboardAgentRow', () => {
const tokens = classTokens(markup)
expect(markup).toContain('aria-label="Waiting for input"')
// Why: the "needs you" states render the amber MessageCircleQuestion glyph
// rather than a bare amber dot (unified across the board/sidebar).
expect(markup).toContain('lucide-message-circle-question')
expect(markup).toContain('lucide-message-circle-question-mark')
expect(tokens).toContain('text-amber-500')
expect(tokens).not.toContain('bg-red-500')
})
@@ -20,6 +20,7 @@ const mocks = vi.hoisted(() => ({
refreshPreflightStatus: vi.fn(),
checkLinearConnection: vi.fn(),
hasPairedMobileDevice: false,
agentBucketCounts: { attention: 0, working: 0, idle: 0 },
dismissMobileOnboardingBadge: vi.fn(),
setSetupGuideSidebarDismissed: vi.fn()
}))
@@ -39,6 +40,10 @@ vi.mock('@/components/activity/useActivityUnreadCount', () => ({
useActivityUnreadCount: () => 0
}))
vi.mock('@/components/dashboard/useAgentBucketCounts', () => ({
useAgentBucketCounts: () => mocks.agentBucketCounts
}))
vi.mock('@/hooks/useShortcutLabel', () => ({
useShortcutKeyComboDetails: () => [{ keys: ['⌘', 'J'], doubleTap: false }]
}))
@@ -203,6 +208,7 @@ describe('SidebarNav', () => {
vi.clearAllMocks()
await i18n.changeLanguage('en')
mocks.hasPairedMobileDevice = false
mocks.agentBucketCounts = { attention: 0, working: 0, idle: 0 }
setSidebarState()
})
@@ -252,6 +258,26 @@ describe('SidebarNav', () => {
expect(queryButtonByText(container, 'Agent Dashboard')).not.toBeNull()
})
it('uses a question glyph only for the Needs You count', async () => {
mocks.agentBucketCounts = { attention: 2, working: 3, idle: 4 }
setSidebarState({
settings: {
...getDefaultSettings('/tmp'),
experimentalAgentDashboardPopout: true
}
})
const container = await renderSidebarNav()
const attention = container.querySelector('[aria-label="Needs You: 2"]')
const working = container.querySelector('[aria-label="Working: 3"]')
const idle = container.querySelector('[aria-label="Idle: 4"]')
expect(attention?.querySelector('.lucide-message-circle-question-mark')).not.toBeNull()
expect(working?.querySelector('.rounded-full')).not.toBeNull()
expect(idle?.querySelector('.rounded-full')).not.toBeNull()
expect(working?.querySelector('svg')).toBeNull()
expect(idle?.querySelector('svg')).toBeNull()
})
it('shows the Mobile entry by default for older settings', () => {
expect(shouldShowMobileButton(null)).toBe(true)
expect(shouldShowMobileButton({})).toBe(true)
@@ -52,10 +52,6 @@ export function shouldShowAutomationsButton(
return settings?.showAutomationsButton !== false
}
// Per-state markers mirror AgentStateDot so the sidebar counts read the same as
// the board: 'attention' (needs you) renders the amber MessageCircleQuestion
// glyph — the same one the board and status dots now use — while the calmer
// buckets stay dots (yellow = working, neutral = idle).
const DASHBOARD_BUCKET_DOT_CLASS: Record<'working' | 'idle', string> = {
working: 'bg-yellow-500',
idle: 'bg-neutral-500/50'
@@ -35,12 +35,10 @@ describe('StatusIndicator', () => {
it('renders permission as an amber question glyph', () => {
const markup = renderMarkup('permission')
// Why: the "needs permission / needs you" state renders the dashboard's
// MessageCircleQuestion glyph rather than a bare amber dot. Assert the
// lucide class hook + amber text color without coupling to the SVG path.
expect(markup).toContain('lucide-message-circle-question')
expect(markup).toContain('lucide-message-circle-question-mark')
expect(markup).toContain('text-amber-500')
expect(markup).not.toContain('bg-amber-500')
expect(markup).not.toContain('data-agent-spinner')
})
it('renders active as full emerald dot', () => {
@@ -41,10 +41,6 @@ const StatusIndicator = React.memo(function StatusIndicator({
}
if (status === 'permission') {
// Why: the "needs permission / needs you" state renders the same
// MessageCircleQuestion glyph the agent dashboard uses, so the sidebar's
// worktree-level status matches the board and agent-row indicators
// instead of showing a bare amber dot. Amber keeps the attention color.
return (
<span
className={cn('inline-flex h-3 w-3 shrink-0 items-center justify-center', className)}
@@ -126,9 +126,7 @@ describe('WorktreeCardStatusSlot', () => {
)
expect(markup).toContain('Needs permission · Unread')
// Why: the permission status renders the amber MessageCircleQuestion glyph
// rather than a bare amber dot.
expect(markup).toContain('lucide-message-circle-question')
expect(markup).toContain('lucide-message-circle-question-mark')
expect(markup).toContain('text-amber-500')
expect(markup).not.toContain('data-worktree-status-lane-unread=""')
expect(markup).not.toContain('data-worktree-unread-alert=""')
@@ -381,7 +379,7 @@ describe('WorktreeCardStatusSlot', () => {
)
expect(markup).toContain('Needs permission')
expect(markup).toContain('lucide-message-circle-question')
expect(markup).toContain('lucide-message-circle-question-mark')
expect(markup).toContain('text-amber-500')
expect(markup).not.toContain('PR checks: Failed')
})
@@ -40,9 +40,7 @@ describe('TerminalTabLeadingIcon', () => {
const markup = renderStatus('permission')
expect(markup).toContain('data-agent-activity-status="permission"')
// Why: the "needs you" states render the amber MessageCircleQuestion glyph
// rather than a bare amber dot (unified across board/sidebar/tabs).
expect(markup).toContain('lucide-message-circle-question')
expect(markup).toContain('lucide-message-circle-question-mark')
expect(markup).toContain('text-amber-500')
expect(markup).not.toContain('bg-red-500')
})