UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

520 lines 32.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const fs = __importStar(require("node:fs")); const path = __importStar(require("node:path")); const childProcess = __importStar(require("node:child_process")); const mcp_config_1 = require("../../../src/utils/clack/mcp-config"); // Mock the clack utils which wrap the prompts vitest_1.vi.mock('../../../src/utils/clack', () => ({ abortIfCancelled: vitest_1.vi.fn((value) => Promise.resolve(value)), showCopyPasteInstructions: vitest_1.vi.fn(), })); // Mock the external dependencies vitest_1.vi.mock('@clack/prompts', () => ({ confirm: vitest_1.vi.fn(), select: vitest_1.vi.fn(), isCancel: vitest_1.vi.fn(() => false), cancel: vitest_1.vi.fn(), log: { success: vitest_1.vi.fn(), info: vitest_1.vi.fn(), warn: vitest_1.vi.fn(), }, })); vitest_1.vi.mock('node:fs'); vitest_1.vi.mock('node:child_process'); (0, vitest_1.describe)('mcp-config', () => { const getMocks = async () => { const clack = await vitest_1.vi.importMock('@clack/prompts'); const clackUtils = await vitest_1.vi.importMock('../../../src/utils/clack'); return { clack, clackUtils }; }; (0, vitest_1.beforeEach)(() => { vitest_1.vi.clearAllMocks(); }); (0, vitest_1.afterEach)(() => { vitest_1.vi.restoreAllMocks(); }); (0, vitest_1.describe)('offerProjectScopedMcpConfig', () => { (0, vitest_1.it)('should return early if user declines MCP config', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select).mockResolvedValue('no'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); (0, vitest_1.expect)(clack.select).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ message: vitest_1.expect.stringContaining('Optionally add a project-scoped MCP server configuration'), options: vitest_1.expect.arrayContaining([ vitest_1.expect.objectContaining({ value: 'yes' }), vitest_1.expect.objectContaining({ value: 'no' }), vitest_1.expect.objectContaining({ value: 'explain' }), ]), initialValue: 'yes', })); }); (0, vitest_1.it)('should configure for Cursor when selected', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('cursor'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockReadFile = vitest_1.vi .fn() .mockRejectedValue(new Error('File not found')); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); (0, vitest_1.expect)(clack.select).toHaveBeenCalledTimes(2); (0, vitest_1.expect)(clack.select).toHaveBeenNthCalledWith(2, vitest_1.expect.objectContaining({ message: 'Which editor do you want to configure?', options: vitest_1.expect.arrayContaining([ vitest_1.expect.objectContaining({ value: 'cursor' }), vitest_1.expect.objectContaining({ value: 'vscode' }), vitest_1.expect.objectContaining({ value: 'claudeCode' }), ]), })); (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json'), vitest_1.expect.stringContaining('"mcpServers"'), 'utf8'); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json')); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration.'); (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('reload your editor')); }); (0, vitest_1.it)('should configure for VS Code when selected', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('vscode'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockReadFile = vitest_1.vi .fn() .mockRejectedValue(new Error('File not found')); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.vscode/mcp.json'), vitest_1.expect.stringContaining('"servers"'), 'utf8'); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.vscode/mcp.json')); }); (0, vitest_1.it)('should configure for Claude Code when selected', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('claudeCode'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockReadFile = vitest_1.vi .fn() .mockRejectedValue(new Error('File not found')); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.mcp.json'), vitest_1.expect.stringContaining('"mcpServers"'), 'utf8'); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.mcp.json')); }); (0, vitest_1.it)('should update existing Cursor config file', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('cursor'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const existingConfig = JSON.stringify({ mcpServers: { OtherServer: { url: 'https://other.example.com', }, }, }); const mockReadFile = vitest_1.vi.fn().mockResolvedValue(existingConfig); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); (0, vitest_1.expect)(mockReadFile).toHaveBeenCalled(); (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json'), vitest_1.expect.stringContaining('Sentry'), 'utf8'); const writtenContent = JSON.parse(mockWriteFile.mock.calls[0][1]); (0, vitest_1.expect)(writtenContent.mcpServers).toHaveProperty('OtherServer'); (0, vitest_1.expect)(writtenContent.mcpServers).toHaveProperty('Sentry'); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Updated .cursor/mcp.json'); }); (0, vitest_1.it)('should update existing VS Code config file', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('vscode'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const existingConfig = JSON.stringify({ servers: { OtherServer: { url: 'https://other.example.com', type: 'http', }, }, }); const mockReadFile = vitest_1.vi.fn().mockResolvedValue(existingConfig); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); const writtenContent = JSON.parse(mockWriteFile.mock.calls[0][1]); (0, vitest_1.expect)(writtenContent.servers).toHaveProperty('OtherServer'); (0, vitest_1.expect)(writtenContent.servers).toHaveProperty('Sentry'); (0, vitest_1.expect)(writtenContent.servers?.Sentry).toHaveProperty('type', 'http'); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Updated .vscode/mcp.json'); }); (0, vitest_1.it)('should update existing Claude Code config file', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('claudeCode'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const existingConfig = JSON.stringify({ mcpServers: { OtherServer: { url: 'https://other.example.com', }, }, }); const mockReadFile = vitest_1.vi.fn().mockResolvedValue(existingConfig); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); const writtenContent = JSON.parse(mockWriteFile.mock.calls[0][1]); (0, vitest_1.expect)(writtenContent.mcpServers).toHaveProperty('OtherServer'); (0, vitest_1.expect)(writtenContent.mcpServers).toHaveProperty('Sentry'); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Updated .mcp.json'); }); (0, vitest_1.it)('should handle file write errors gracefully for Cursor', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('cursor'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockReadFile = vitest_1.vi .fn() .mockRejectedValue(new Error('File not found')); const mockWriteFile = vitest_1.vi .fn() .mockRejectedValue(new Error('Permission denied')); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined(); (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config automatically')); (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ filename: path.join('.cursor', 'mcp.json'), codeSnippet: vitest_1.expect.stringContaining('mcpServers'), hint: 'create the file if it does not exist', })); }); (0, vitest_1.it)('should handle file write errors gracefully for VS Code', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('vscode'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockReadFile = vitest_1.vi .fn() .mockRejectedValue(new Error('File not found')); const mockWriteFile = vitest_1.vi .fn() .mockRejectedValue(new Error('Permission denied')); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined(); (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ filename: path.join('.vscode', 'mcp.json'), codeSnippet: vitest_1.expect.stringContaining('servers'), hint: 'create the file if it does not exist', })); }); (0, vitest_1.it)('should handle file write errors gracefully for Claude Code', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('claudeCode'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockReadFile = vitest_1.vi .fn() .mockRejectedValue(new Error('File not found')); const mockWriteFile = vitest_1.vi .fn() .mockRejectedValue(new Error('Permission denied')); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined(); (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ filename: '.mcp.json', codeSnippet: vitest_1.expect.stringContaining('mcpServers'), hint: 'create the file if it does not exist', })); }); (0, vitest_1.it)('should handle update errors and show copy-paste instructions', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('cursor'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); // Mock existing file and simulate write error during update const existingConfig = JSON.stringify({ mcpServers: { OtherServer: { url: 'https://other.example.com', }, }, }); const mockReadFile = vitest_1.vi.fn().mockResolvedValue(existingConfig); const mockWriteFile = vitest_1.vi .fn() .mockRejectedValue(new Error('Write failed during update')); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined(); (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config automatically')); (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalled(); }); (0, vitest_1.it)('should handle mkdirSync errors', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('cursor'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockReadFile = vitest_1.vi .fn() .mockRejectedValue(new Error('File not found')); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn().mockImplementation(() => { throw new Error('Permission denied'); }); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, vitest_1.expect)((0, mcp_config_1.offerProjectScopedMcpConfig)()).resolves.toBeUndefined(); (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config automatically')); (0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalled(); }); (0, vitest_1.it)('should create config with empty servers/mcpServers when existing config lacks them', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('vscode'); vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const existingConfig = JSON.stringify({ otherProperty: 'value', }); const mockReadFile = vitest_1.vi.fn().mockResolvedValue(existingConfig); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); const writtenContent = JSON.parse(mockWriteFile.mock.calls[0][1]); (0, vitest_1.expect)(writtenContent).toHaveProperty('otherProperty', 'value'); (0, vitest_1.expect)(writtenContent).toHaveProperty('servers'); (0, vitest_1.expect)(writtenContent.servers).toHaveProperty('Sentry'); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Updated .vscode/mcp.json'); }); (0, vitest_1.it)('should show config for JetBrains IDEs with clipboard copy', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('jetbrains') .mockResolvedValueOnce(true); // For the clipboard copy prompt vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); // Mock clipboard copy const mockSpawn = vitest_1.vi.fn().mockReturnValue({ stdin: { write: vitest_1.vi.fn(), end: vitest_1.vi.fn(), }, on: vitest_1.vi.fn((event, callback) => { if (event === 'close') callback(0); }), }); vitest_1.vi.spyOn(childProcess, 'spawn').mockImplementation(mockSpawn); // Mock console.log to capture output // eslint-disable-next-line @typescript-eslint/no-empty-function const consoleSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => { }); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('JetBrains IDEs')); (0, vitest_1.expect)(consoleSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('mcpServers')); // Should ask to copy to clipboard (0, vitest_1.expect)(clack.select).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ message: 'Copy configuration to clipboard?', })); (0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Configuration copied to clipboard!'); consoleSpy.mockRestore(); }); (0, vitest_1.it)('should show generic config for unsupported IDEs with clipboard copy', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('other') .mockResolvedValueOnce(true); // For the clipboard copy prompt vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); // Mock clipboard copy failure to test fallback const mockSpawn = vitest_1.vi.fn().mockReturnValue({ stdin: { write: vitest_1.vi.fn(), end: vitest_1.vi.fn(), }, on: vitest_1.vi.fn((event, callback) => { if (event === 'error') callback(); }), }); vitest_1.vi.spyOn(childProcess, 'spawn').mockImplementation(mockSpawn); // Mock console.log to capture output // eslint-disable-next-line @typescript-eslint/no-empty-function const consoleSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => { }); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Generic MCP configuration')); (0, vitest_1.expect)(consoleSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining('mcpServers')); // Should ask to copy to clipboard (0, vitest_1.expect)(clack.select).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ message: 'Copy configuration to clipboard?', })); // Since clipboard copy failed, should show warning (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to copy to clipboard')); consoleSpy.mockRestore(); }); (0, vitest_1.it)('should handle clipboard copy failure gracefully for JetBrains', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('jetbrains') .mockResolvedValueOnce(true); // For clipboard copy prompt vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); // Mock clipboard copy to throw error const mockSpawn = vitest_1.vi.fn().mockImplementation(() => { throw new Error('Clipboard not available'); }); vitest_1.vi.spyOn(childProcess, 'spawn').mockImplementation(mockSpawn); // Mock console.log to capture output // eslint-disable-next-line @typescript-eslint/no-empty-function const consoleSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => { }); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); // Should ask to copy to clipboard (0, vitest_1.expect)(clack.select).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ message: 'Copy configuration to clipboard?', })); // Should show warning when clipboard fails (0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to copy to clipboard')); consoleSpy.mockRestore(); }); (0, vitest_1.it)('should show MCP explanation when user selects "What is MCP?"', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('explain') // User selects "What is MCP?" .mockResolvedValueOnce(true) // User selects "Yes" after explanation .mockResolvedValueOnce('cursor'); // User selects Cursor vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockReadFile = vitest_1.vi .fn() .mockRejectedValue(new Error('File not found')); const mockWriteFile = vitest_1.vi.fn().mockResolvedValue(undefined); const mockMkdirSync = vitest_1.vi.fn(); vitest_1.vi.spyOn(fs.promises, 'readFile').mockImplementation(mockReadFile); vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile); vitest_1.vi.spyOn(fs, 'mkdirSync').mockImplementation(mockMkdirSync); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); // Should show MCP explanation (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('What is MCP')); (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('AI assistants')); (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('https://docs.sentry.io/product/sentry-mcp/')); // Should ask again after explanation (0, vitest_1.expect)(clack.select).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ message: 'Would you like to configure MCP for your IDE now?', })); // Should proceed with normal flow (0, vitest_1.expect)(mockWriteFile).toHaveBeenCalled(); }); (0, vitest_1.it)('should respect user choice not to copy to clipboard', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('yes') .mockResolvedValueOnce('jetbrains') .mockResolvedValueOnce(false); // User declines to copy to clipboard vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); const mockSpawn = vitest_1.vi.fn(); vitest_1.vi.spyOn(childProcess, 'spawn').mockImplementation(mockSpawn); // Mock console.log to capture output // eslint-disable-next-line @typescript-eslint/no-empty-function const consoleSpy = vitest_1.vi.spyOn(console, 'log').mockImplementation(() => { }); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); // Should ask to copy to clipboard (0, vitest_1.expect)(clack.select).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ message: 'Copy configuration to clipboard?', })); // Should NOT attempt to copy when user declines (0, vitest_1.expect)(mockSpawn).not.toHaveBeenCalled(); // Should NOT show success or warning messages (0, vitest_1.expect)(clack.log.success).not.toHaveBeenCalledWith('Configuration copied to clipboard!'); (0, vitest_1.expect)(clack.log.warn).not.toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to copy to clipboard')); consoleSpy.mockRestore(); }); (0, vitest_1.it)('should exit if user declines after MCP explanation', async () => { const { clack, clackUtils } = await getMocks(); vitest_1.vi.mocked(clack.select) .mockResolvedValueOnce('explain') // User selects "What is MCP?" .mockResolvedValueOnce(false); // User selects "No" after explanation vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value)); await (0, mcp_config_1.offerProjectScopedMcpConfig)(); // Should show MCP explanation (0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('What is MCP')); // Should ask again after explanation (0, vitest_1.expect)(clack.select).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ message: 'Would you like to configure MCP for your IDE now?', })); // Should NOT proceed with editor selection (0, vitest_1.expect)(clack.select).not.toHaveBeenCalledWith(vitest_1.expect.objectContaining({ message: 'Which editor do you want to configure?', })); }); }); }); //# sourceMappingURL=mcp-config.test.js.map