|
| 1 | +import fs from 'fs'; |
| 2 | +import os from 'os'; |
| 3 | +import path from 'path'; |
| 4 | +import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'; |
| 5 | + |
| 6 | +const mocks = vi.hoisted(()=>({ |
| 7 | + checkbox: vi.fn(), |
| 8 | + select: vi.fn(), |
| 9 | + confirm: vi.fn(), |
| 10 | + get_api_key: vi.fn(), |
| 11 | + dim: vi.fn((msg: string)=>msg), |
| 12 | + green: vi.fn((msg: string)=>msg), |
| 13 | + red: vi.fn((msg: string)=>msg), |
| 14 | + warn: vi.fn(), |
| 15 | +})); |
| 16 | + |
| 17 | +vi.mock('@inquirer/prompts', ()=>({ |
| 18 | + checkbox: mocks.checkbox, |
| 19 | + select: mocks.select, |
| 20 | + confirm: mocks.confirm, |
| 21 | +})); |
| 22 | + |
| 23 | +vi.mock('../../utils/credentials', ()=>({ |
| 24 | + get_api_key: mocks.get_api_key, |
| 25 | +})); |
| 26 | + |
| 27 | +vi.mock('../../utils/output', ()=>({ |
| 28 | + dim: mocks.dim, |
| 29 | + green: mocks.green, |
| 30 | + red: mocks.red, |
| 31 | + warn: mocks.warn, |
| 32 | +})); |
| 33 | + |
| 34 | +import {run_add_mcp} from '../../commands/add-mcp'; |
| 35 | + |
| 36 | +const get_expected_entry = (api_key: string)=>({ |
| 37 | + command: 'npx', |
| 38 | + args: ['@brightdata/mcp'], |
| 39 | + env: { |
| 40 | + API_TOKEN: api_key, |
| 41 | + }, |
| 42 | +}); |
| 43 | + |
| 44 | +const mk_tmp_dir = ()=>fs.mkdtempSync(path.join(os.tmpdir(), |
| 45 | + 'brightdata-add-mcp-')); |
| 46 | + |
| 47 | +const read_json = (file_path: string)=>JSON.parse(fs.readFileSync(file_path, |
| 48 | + 'utf8')); |
| 49 | + |
| 50 | +describe('commands/add-mcp', ()=>{ |
| 51 | + let tmp_dir = ''; |
| 52 | + let home_dir = ''; |
| 53 | + let project_dir = ''; |
| 54 | + let codex_home = ''; |
| 55 | + let original_cwd = ''; |
| 56 | + let stdin_tty: PropertyDescriptor|undefined; |
| 57 | + let stdout_tty: PropertyDescriptor|undefined; |
| 58 | + |
| 59 | + beforeEach(()=>{ |
| 60 | + vi.clearAllMocks(); |
| 61 | + tmp_dir = mk_tmp_dir(); |
| 62 | + home_dir = path.join(tmp_dir, 'home'); |
| 63 | + project_dir = path.join(tmp_dir, 'project'); |
| 64 | + codex_home = path.join(tmp_dir, 'codex-home'); |
| 65 | + original_cwd = process.cwd(); |
| 66 | + stdin_tty = Object.getOwnPropertyDescriptor(process.stdin, 'isTTY'); |
| 67 | + stdout_tty = Object.getOwnPropertyDescriptor(process.stdout, 'isTTY'); |
| 68 | + |
| 69 | + fs.mkdirSync(home_dir, {recursive: true}); |
| 70 | + fs.mkdirSync(project_dir, {recursive: true}); |
| 71 | + process.chdir(project_dir); |
| 72 | + process.env['CODEX_HOME'] = codex_home; |
| 73 | + vi.spyOn(os, 'homedir').mockReturnValue(home_dir); |
| 74 | + Object.defineProperty(process.stdin, 'isTTY', { |
| 75 | + value: true, |
| 76 | + configurable: true, |
| 77 | + }); |
| 78 | + Object.defineProperty(process.stdout, 'isTTY', { |
| 79 | + value: true, |
| 80 | + configurable: true, |
| 81 | + }); |
| 82 | + |
| 83 | + mocks.get_api_key.mockReturnValue('test_api_key'); |
| 84 | + mocks.checkbox.mockResolvedValue(['claude-code', 'cursor', 'codex']); |
| 85 | + mocks.select.mockResolvedValue('project'); |
| 86 | + mocks.confirm.mockResolvedValue(true); |
| 87 | + vi.spyOn(process.stderr, 'write').mockImplementation(()=>true); |
| 88 | + }); |
| 89 | + |
| 90 | + afterEach(()=>{ |
| 91 | + process.chdir(original_cwd); |
| 92 | + if (stdin_tty) |
| 93 | + Object.defineProperty(process.stdin, 'isTTY', stdin_tty); |
| 94 | + if (stdout_tty) |
| 95 | + Object.defineProperty(process.stdout, 'isTTY', stdout_tty); |
| 96 | + delete process.env['CODEX_HOME']; |
| 97 | + vi.restoreAllMocks(); |
| 98 | + if (tmp_dir) |
| 99 | + fs.rmSync(tmp_dir, {recursive: true, force: true}); |
| 100 | + }); |
| 101 | + |
| 102 | + it('writes selected agent configs in the full interactive flow', async()=>{ |
| 103 | + await run_add_mcp(); |
| 104 | + |
| 105 | + expect(mocks.checkbox).toHaveBeenCalledOnce(); |
| 106 | + expect(mocks.select).toHaveBeenCalledOnce(); |
| 107 | + expect(mocks.confirm).not.toHaveBeenCalled(); |
| 108 | + expect(read_json(path.join(project_dir, '.claude', 'settings.json'))) |
| 109 | + .toEqual({ |
| 110 | + mcpServers: { |
| 111 | + 'bright-data': get_expected_entry('test_api_key'), |
| 112 | + }, |
| 113 | + }); |
| 114 | + expect(read_json(path.join(project_dir, '.cursor', 'mcp.json'))) |
| 115 | + .toEqual({ |
| 116 | + mcpServers: { |
| 117 | + 'bright-data': get_expected_entry('test_api_key'), |
| 118 | + }, |
| 119 | + }); |
| 120 | + expect(read_json(path.join(codex_home, 'mcp.json'))) |
| 121 | + .toEqual({ |
| 122 | + mcpServers: { |
| 123 | + 'bright-data': get_expected_entry('test_api_key'), |
| 124 | + }, |
| 125 | + }); |
| 126 | + expect(fs.existsSync(path.join(home_dir, '.claude.json'))).toBe(false); |
| 127 | + expect(fs.existsSync(path.join(home_dir, '.cursor', 'mcp.json'))) |
| 128 | + .toBe(false); |
| 129 | + }); |
| 130 | + |
| 131 | + it('warns and overwrites invalid JSON after confirmation', async()=>{ |
| 132 | + const cursor_config = path.join(project_dir, '.cursor', 'mcp.json'); |
| 133 | + |
| 134 | + fs.mkdirSync(path.dirname(cursor_config), {recursive: true}); |
| 135 | + fs.writeFileSync(cursor_config, '{invalid-json'); |
| 136 | + mocks.checkbox.mockResolvedValue(['cursor']); |
| 137 | + mocks.select.mockResolvedValue('project'); |
| 138 | + mocks.confirm.mockResolvedValue(true); |
| 139 | + |
| 140 | + await run_add_mcp(); |
| 141 | + |
| 142 | + expect(mocks.warn).toHaveBeenCalledWith( |
| 143 | + expect.stringContaining('Invalid JSON in '+cursor_config) |
| 144 | + ); |
| 145 | + expect(mocks.confirm).toHaveBeenCalledWith({ |
| 146 | + message: 'Overwrite invalid config at '+cursor_config+'?', |
| 147 | + default: false, |
| 148 | + }); |
| 149 | + expect(read_json(cursor_config)).toEqual({ |
| 150 | + mcpServers: { |
| 151 | + 'bright-data': get_expected_entry('test_api_key'), |
| 152 | + }, |
| 153 | + }); |
| 154 | + }); |
| 155 | +}); |
0 commit comments