external.test.ts
1 import { beforeEach, describe, expect, it, vi } from 'vitest'; 2 3 const { mockExecFileSync, mockPlatform } = vi.hoisted(() => ({ 4 mockExecFileSync: vi.fn(), 5 mockPlatform: vi.fn(() => 'darwin'), 6 })); 7 8 vi.mock('node:child_process', () => ({ 9 spawnSync: vi.fn(), 10 execFileSync: mockExecFileSync, 11 })); 12 13 vi.mock('node:os', async () => { 14 const actual = await vi.importActual<typeof import('node:os')>('node:os'); 15 return { 16 ...actual, 17 platform: mockPlatform, 18 }; 19 }); 20 21 import { installExternalCli, parseCommand, type ExternalCliConfig } from './external.js'; 22 23 describe('parseCommand', () => { 24 it('splits binaries and quoted arguments without invoking a shell', () => { 25 expect(parseCommand('npm install -g "@scope/tool name"')).toEqual({ 26 binary: 'npm', 27 args: ['install', '-g', '@scope/tool name'], 28 }); 29 }); 30 31 it('rejects shell operators', () => { 32 expect(() => parseCommand('brew install gh && rm -rf /')).toThrow( 33 'Install command contains unsafe shell operators', 34 ); 35 }); 36 37 it('rejects command substitution and multiline input', () => { 38 expect(() => parseCommand('brew install $(whoami)')).toThrow( 39 'Install command contains unsafe shell operators', 40 ); 41 expect(() => parseCommand('brew install gh\nrm -rf /')).toThrow( 42 'Install command contains unsafe shell operators', 43 ); 44 }); 45 }); 46 47 describe('installExternalCli', () => { 48 const cli: ExternalCliConfig = { 49 name: 'readwise', 50 binary: 'readwise', 51 install: { 52 default: 'npm install -g @readwiseio/readwise-cli', 53 }, 54 }; 55 56 beforeEach(() => { 57 mockExecFileSync.mockReset(); 58 mockPlatform.mockReturnValue('darwin'); 59 }); 60 61 it('retries with .cmd on Windows when the bare binary is unavailable', () => { 62 mockPlatform.mockReturnValue('win32'); 63 mockExecFileSync 64 .mockImplementationOnce(() => { 65 const err = new Error('not found') as NodeJS.ErrnoException; 66 err.code = 'ENOENT'; 67 throw err; 68 }) 69 .mockReturnValueOnce(Buffer.from('')); 70 71 expect(installExternalCli(cli)).toBe(true); 72 expect(mockExecFileSync).toHaveBeenNthCalledWith( 73 1, 74 'npm', 75 ['install', '-g', '@readwiseio/readwise-cli'], 76 { stdio: 'inherit' }, 77 ); 78 expect(mockExecFileSync).toHaveBeenNthCalledWith( 79 2, 80 'npm.cmd', 81 ['install', '-g', '@readwiseio/readwise-cli'], 82 { stdio: 'inherit' }, 83 ); 84 }); 85 86 it('does not mask non-ENOENT failures', () => { 87 mockPlatform.mockReturnValue('win32'); 88 mockExecFileSync.mockImplementationOnce(() => { 89 const err = new Error('permission denied') as NodeJS.ErrnoException; 90 err.code = 'EACCES'; 91 throw err; 92 }); 93 94 expect(installExternalCli(cli)).toBe(false); 95 expect(mockExecFileSync).toHaveBeenCalledTimes(1); 96 }); 97 });