diff --git a/packages/cli/src/ui/commands/btwCommand.test.ts b/packages/cli/src/ui/commands/btwCommand.test.ts new file mode 100644 index 0000000000..87e8578bca --- /dev/null +++ b/packages/cli/src/ui/commands/btwCommand.test.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { btwCommand } from './btwCommand.js'; +import { CommandKind } from './types.js'; +import type { CommandContext } from './types.js'; + +describe('btwCommand', () => { + it('has the correct metadata', () => { + expect(btwCommand.name).toBe('btw'); + expect(btwCommand.kind).toBe(CommandKind.BUILT_IN); + expect(btwCommand.autoExecute).toBe(true); + expect(btwCommand.isSafeConcurrent).toBe(true); + }); + + it('returns an error message when args are empty', () => { + const context = {} as CommandContext; + const result = btwCommand.action!(context, ' '); + expect(result).toEqual({ + type: 'message', + messageType: 'error', + content: 'Please provide a question, e.g. /btw what is this regex doing?', + }); + }); + + it('returns a btw action when query is provided', () => { + const context = {} as CommandContext; + const result = btwCommand.action!(context, ' what is this regex doing? '); + expect(result).toEqual({ + type: 'btw', + query: 'what is this regex doing?', + }); + }); +}); diff --git a/packages/cli/src/ui/components/BtwDisplay.test.tsx b/packages/cli/src/ui/components/BtwDisplay.test.tsx new file mode 100644 index 0000000000..88fe96db74 --- /dev/null +++ b/packages/cli/src/ui/components/BtwDisplay.test.tsx @@ -0,0 +1,88 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { renderWithProviders } from '../../test-utils/render.js'; +import { BtwDisplay } from './BtwDisplay.js'; +import { StreamingState } from '../types.js'; +import type { UIState } from '../contexts/UIStateContext.js'; + +describe('BtwDisplay', () => { + const defaultMockUiState = { + renderMarkdown: true, + } as unknown as Partial; + + it('renders nothing when query is empty', async () => { + const { lastFrame, unmount } = await renderWithProviders( + , + { uiState: defaultMockUiState }, + ); + expect(lastFrame({ allowEmpty: true })).toBe(''); + unmount(); + }); + + it('renders query and response', async () => { + const { lastFrame, unmount } = await renderWithProviders( + , + { uiState: defaultMockUiState }, + ); + const frame = lastFrame(); + expect(frame).toContain('What is life?'); + expect(frame).toContain('Life is 42.'); + expect(frame).toContain('BY THE WAY'); + unmount(); + }); + + it('renders error message when error is provided', async () => { + const { lastFrame, unmount } = await renderWithProviders( + , + { uiState: defaultMockUiState }, + ); + const frame = lastFrame(); + expect(frame).toContain('An API error occurred.'); + expect(frame).not.toContain('Life is 42.'); + unmount(); + }); + + it('renders a spinner when streaming', async () => { + const { lastFrame, unmount } = await renderWithProviders( + , + { + uiState: { + ...defaultMockUiState, + streamingState: StreamingState.Responding, + }, + }, + ); + const frame = lastFrame(); + expect(frame).toContain('⠋'); // Assuming standard spinner frame + unmount(); + }); +}); diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 7a241691e8..b274d0decf 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -5097,6 +5097,92 @@ describe('InputPrompt', () => { }, ); }); + + describe('Btw dismiss behavior', () => { + it('dismisses Btw on ESC key press', async () => { + const dismissBtw = vi.fn(); + const { stdin, unmount } = await renderWithProviders( + , + { + uiState: { + btwState: { + isActive: true, + query: '', + response: '', + isStreaming: false, + error: null, + }, + }, + uiActions: { dismissBtw }, + }, + ); + + await act(async () => { + stdin.write('\x1B'); // ESC + }); + + await waitFor(() => { + expect(dismissBtw).toHaveBeenCalled(); + }); + unmount(); + }); + + it('dismisses Btw on Enter key press', async () => { + const dismissBtw = vi.fn(); + const { stdin, unmount } = await renderWithProviders( + , + { + uiState: { + btwState: { + isActive: true, + query: '', + response: '', + isStreaming: false, + error: null, + }, + }, + uiActions: { dismissBtw }, + }, + ); + + await act(async () => { + stdin.write('\r'); // Enter + }); + + await waitFor(() => { + expect(dismissBtw).toHaveBeenCalled(); + }); + unmount(); + }); + + it('dismisses Btw on Space key press when buffer is empty', async () => { + const dismissBtw = vi.fn(); + const { stdin, unmount } = await renderWithProviders( + , + { + uiState: { + btwState: { + isActive: true, + query: '', + response: '', + isStreaming: false, + error: null, + }, + }, + uiActions: { dismissBtw }, + }, + ); + + await act(async () => { + stdin.write(' '); // Space + }); + + await waitFor(() => { + expect(dismissBtw).toHaveBeenCalled(); + }); + unmount(); + }); + }); }); function clean(str: string | undefined): string { diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx b/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx index ec4aa00677..2032b30acc 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx @@ -644,6 +644,31 @@ describe('useSlashCommandProcessor', () => { expect(mockSetQuittingMessages).toHaveBeenCalledWith(['bye']); }); + + it('should handle a "btw" action', async () => { + const btwAction = vi + .fn() + .mockResolvedValue({ type: 'btw', query: 'some query' }); + const command = createTestCommand({ + name: 'side_question', + action: btwAction, + }); + const result = await setupProcessorHook({ + builtinCommands: [command], + }); + + await waitFor(() => expect(result.current.slashCommands).toHaveLength(1)); + + let processedResult; + await act(async () => { + processedResult = await result.current.handleSlashCommand( + '/side_question some args', + ); + }); + + expect(processedResult).toEqual({ type: 'btw', query: 'some query' }); + }); + it('should handle "submit_prompt" action returned from a file-based command', async () => { const fileCommand = createTestCommand( {