summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/utils/commandUtils.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/utils/commandUtils.test.ts')
-rw-r--r--packages/cli/src/ui/utils/commandUtils.test.ts345
1 files changed, 345 insertions, 0 deletions
diff --git a/packages/cli/src/ui/utils/commandUtils.test.ts b/packages/cli/src/ui/utils/commandUtils.test.ts
new file mode 100644
index 00000000..158a5f7a
--- /dev/null
+++ b/packages/cli/src/ui/utils/commandUtils.test.ts
@@ -0,0 +1,345 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { vi, describe, it, expect, beforeEach, Mock } from 'vitest';
+import { spawn } from 'child_process';
+import { EventEmitter } from 'events';
+import {
+ isAtCommand,
+ isSlashCommand,
+ copyToClipboard,
+} from './commandUtils.js';
+
+// Mock child_process
+vi.mock('child_process');
+
+// Mock process.platform for platform-specific tests
+const mockProcess = vi.hoisted(() => ({
+ platform: 'darwin',
+}));
+
+vi.stubGlobal('process', {
+ ...process,
+ get platform() {
+ return mockProcess.platform;
+ },
+});
+
+interface MockChildProcess extends EventEmitter {
+ stdin: EventEmitter & {
+ write: Mock;
+ end: Mock;
+ };
+ stderr: EventEmitter;
+}
+
+describe('commandUtils', () => {
+ let mockSpawn: Mock;
+ let mockChild: MockChildProcess;
+
+ beforeEach(async () => {
+ vi.clearAllMocks();
+ // Dynamically import and set up spawn mock
+ const { spawn } = await import('child_process');
+ mockSpawn = spawn as Mock;
+
+ // Create mock child process with stdout/stderr emitters
+ mockChild = Object.assign(new EventEmitter(), {
+ stdin: Object.assign(new EventEmitter(), {
+ write: vi.fn(),
+ end: vi.fn(),
+ }),
+ stderr: new EventEmitter(),
+ }) as MockChildProcess;
+
+ mockSpawn.mockReturnValue(mockChild as unknown as ReturnType<typeof spawn>);
+ });
+
+ describe('isAtCommand', () => {
+ it('should return true when query starts with @', () => {
+ expect(isAtCommand('@file')).toBe(true);
+ expect(isAtCommand('@path/to/file')).toBe(true);
+ expect(isAtCommand('@')).toBe(true);
+ });
+
+ it('should return true when query contains @ preceded by whitespace', () => {
+ expect(isAtCommand('hello @file')).toBe(true);
+ expect(isAtCommand('some text @path/to/file')).toBe(true);
+ expect(isAtCommand(' @file')).toBe(true);
+ });
+
+ it('should return false when query does not start with @ and has no spaced @', () => {
+ expect(isAtCommand('file')).toBe(false);
+ expect(isAtCommand('hello')).toBe(false);
+ expect(isAtCommand('')).toBe(false);
+ expect(isAtCommand('[email protected]')).toBe(false);
+ expect(isAtCommand('user@host')).toBe(false);
+ });
+
+ it('should return false when @ is not preceded by whitespace', () => {
+ expect(isAtCommand('hello@file')).toBe(false);
+ expect(isAtCommand('text@path')).toBe(false);
+ });
+ });
+
+ describe('isSlashCommand', () => {
+ it('should return true when query starts with /', () => {
+ expect(isSlashCommand('/help')).toBe(true);
+ expect(isSlashCommand('/memory show')).toBe(true);
+ expect(isSlashCommand('/clear')).toBe(true);
+ expect(isSlashCommand('/')).toBe(true);
+ });
+
+ it('should return false when query does not start with /', () => {
+ expect(isSlashCommand('help')).toBe(false);
+ expect(isSlashCommand('memory show')).toBe(false);
+ expect(isSlashCommand('')).toBe(false);
+ expect(isSlashCommand('path/to/file')).toBe(false);
+ expect(isSlashCommand(' /help')).toBe(false);
+ });
+ });
+
+ describe('copyToClipboard', () => {
+ describe('on macOS (darwin)', () => {
+ beforeEach(() => {
+ mockProcess.platform = 'darwin';
+ });
+
+ it('should successfully copy text to clipboard using pbcopy', async () => {
+ const testText = 'Hello, world!';
+
+ // Simulate successful execution
+ setTimeout(() => {
+ mockChild.emit('close', 0);
+ }, 0);
+
+ await copyToClipboard(testText);
+
+ expect(mockSpawn).toHaveBeenCalledWith('pbcopy', []);
+ expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
+ expect(mockChild.stdin.end).toHaveBeenCalled();
+ });
+
+ it('should handle pbcopy command failure', async () => {
+ const testText = 'Hello, world!';
+
+ // Simulate command failure
+ setTimeout(() => {
+ mockChild.stderr.emit('data', 'Command not found');
+ mockChild.emit('close', 1);
+ }, 0);
+
+ await expect(copyToClipboard(testText)).rejects.toThrow(
+ "'pbcopy' exited with code 1: Command not found",
+ );
+ });
+
+ it('should handle spawn error', async () => {
+ const testText = 'Hello, world!';
+
+ setTimeout(() => {
+ mockChild.emit('error', new Error('spawn error'));
+ }, 0);
+
+ await expect(copyToClipboard(testText)).rejects.toThrow('spawn error');
+ });
+
+ it('should handle stdin write error', async () => {
+ const testText = 'Hello, world!';
+
+ setTimeout(() => {
+ mockChild.stdin.emit('error', new Error('stdin error'));
+ }, 0);
+
+ await expect(copyToClipboard(testText)).rejects.toThrow('stdin error');
+ });
+ });
+
+ describe('on Windows (win32)', () => {
+ beforeEach(() => {
+ mockProcess.platform = 'win32';
+ });
+
+ it('should successfully copy text to clipboard using clip', async () => {
+ const testText = 'Hello, world!';
+
+ setTimeout(() => {
+ mockChild.emit('close', 0);
+ }, 0);
+
+ await copyToClipboard(testText);
+
+ expect(mockSpawn).toHaveBeenCalledWith('clip', []);
+ expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
+ expect(mockChild.stdin.end).toHaveBeenCalled();
+ });
+ });
+
+ describe('on Linux', () => {
+ beforeEach(() => {
+ mockProcess.platform = 'linux';
+ });
+
+ it('should successfully copy text to clipboard using xclip', async () => {
+ const testText = 'Hello, world!';
+
+ setTimeout(() => {
+ mockChild.emit('close', 0);
+ }, 0);
+
+ await copyToClipboard(testText);
+
+ expect(mockSpawn).toHaveBeenCalledWith('xclip', [
+ '-selection',
+ 'clipboard',
+ ]);
+ expect(mockChild.stdin.write).toHaveBeenCalledWith(testText);
+ expect(mockChild.stdin.end).toHaveBeenCalled();
+ });
+
+ it('should fallback to xsel when xclip fails', async () => {
+ const testText = 'Hello, world!';
+ let callCount = 0;
+
+ mockSpawn.mockImplementation(() => {
+ const child = Object.assign(new EventEmitter(), {
+ stdin: Object.assign(new EventEmitter(), {
+ write: vi.fn(),
+ end: vi.fn(),
+ }),
+ stderr: new EventEmitter(),
+ }) as MockChildProcess;
+
+ setTimeout(() => {
+ if (callCount === 0) {
+ // First call (xclip) fails
+ child.stderr.emit('data', 'xclip not found');
+ child.emit('close', 1);
+ callCount++;
+ } else {
+ // Second call (xsel) succeeds
+ child.emit('close', 0);
+ }
+ }, 0);
+
+ return child as unknown as ReturnType<typeof spawn>;
+ });
+
+ await copyToClipboard(testText);
+
+ expect(mockSpawn).toHaveBeenCalledTimes(2);
+ expect(mockSpawn).toHaveBeenNthCalledWith(1, 'xclip', [
+ '-selection',
+ 'clipboard',
+ ]);
+ expect(mockSpawn).toHaveBeenNthCalledWith(2, 'xsel', [
+ '--clipboard',
+ '--input',
+ ]);
+ });
+
+ it('should throw error when both xclip and xsel fail', async () => {
+ const testText = 'Hello, world!';
+ let callCount = 0;
+
+ mockSpawn.mockImplementation(() => {
+ const child = Object.assign(new EventEmitter(), {
+ stdin: Object.assign(new EventEmitter(), {
+ write: vi.fn(),
+ end: vi.fn(),
+ }),
+ stderr: new EventEmitter(),
+ });
+
+ setTimeout(() => {
+ if (callCount === 0) {
+ // First call (xclip) fails
+ child.stderr.emit('data', 'xclip command not found');
+ child.emit('close', 1);
+ callCount++;
+ } else {
+ // Second call (xsel) fails
+ child.stderr.emit('data', 'xsel command not found');
+ child.emit('close', 1);
+ }
+ }, 0);
+
+ return child as unknown as ReturnType<typeof spawn>;
+ });
+
+ await expect(copyToClipboard(testText)).rejects.toThrow(
+ /All copy commands failed/,
+ );
+
+ expect(mockSpawn).toHaveBeenCalledTimes(2);
+ });
+ });
+
+ describe('on unsupported platform', () => {
+ beforeEach(() => {
+ mockProcess.platform = 'unsupported';
+ });
+
+ it('should throw error for unsupported platform', async () => {
+ await expect(copyToClipboard('test')).rejects.toThrow(
+ 'Unsupported platform: unsupported',
+ );
+ });
+ });
+
+ describe('error handling', () => {
+ beforeEach(() => {
+ mockProcess.platform = 'darwin';
+ });
+
+ it('should handle command exit without stderr', async () => {
+ const testText = 'Hello, world!';
+
+ setTimeout(() => {
+ mockChild.emit('close', 1);
+ }, 0);
+
+ await expect(copyToClipboard(testText)).rejects.toThrow(
+ "'pbcopy' exited with code 1",
+ );
+ });
+
+ it('should handle empty text', async () => {
+ setTimeout(() => {
+ mockChild.emit('close', 0);
+ }, 0);
+
+ await copyToClipboard('');
+
+ expect(mockChild.stdin.write).toHaveBeenCalledWith('');
+ });
+
+ it('should handle multiline text', async () => {
+ const multilineText = 'Line 1\nLine 2\nLine 3';
+
+ setTimeout(() => {
+ mockChild.emit('close', 0);
+ }, 0);
+
+ await copyToClipboard(multilineText);
+
+ expect(mockChild.stdin.write).toHaveBeenCalledWith(multilineText);
+ });
+
+ it('should handle special characters', async () => {
+ const specialText = 'Special chars: !@#$%^&*()_+-=[]{}|;:,.<>?';
+
+ setTimeout(() => {
+ mockChild.emit('close', 0);
+ }, 0);
+
+ await copyToClipboard(specialText);
+
+ expect(mockChild.stdin.write).toHaveBeenCalledWith(specialText);
+ });
+ });
+ });
+});