@sentry/wizard
Version:
Sentry wizard helping you to configure your project
645 lines • 43.3 kB
JavaScript
"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(),
multiselect: 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');
vitest_1.vi.mocked(clack.multiselect).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.multiselect).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
message: 'Which editor(s) 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 for Cursor.');
(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');
vitest_1.vi.mocked(clack.multiselect).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');
vitest_1.vi.mocked(clack.multiselect).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');
vitest_1.vi.mocked(clack.multiselect).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');
vitest_1.vi.mocked(clack.multiselect).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');
vitest_1.vi.mocked(clack.multiselect).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 configure for OpenCode when selected', async () => {
const { clack, clackUtils } = await getMocks();
vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['openCode']);
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('opencode.json'), vitest_1.expect.stringContaining('"mcp"'), 'utf8');
// Verify the written content has the correct structure for OpenCode
const writtenContent = JSON.parse(mockWriteFile.mock.calls[0][1]);
(0, vitest_1.expect)(writtenContent.$schema).toBe('https://opencode.ai/config.json');
(0, vitest_1.expect)(writtenContent.mcp).toHaveProperty('Sentry');
(0, vitest_1.expect)(writtenContent.mcp?.Sentry).toHaveProperty('type', 'remote');
(0, vitest_1.expect)(writtenContent.mcp?.Sentry).toHaveProperty('url');
(0, vitest_1.expect)(writtenContent.mcp?.Sentry?.oauth).toEqual({});
(0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('opencode.json'));
(0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for OpenCode.');
(0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith(vitest_1.expect.stringContaining('restart OpenCode'));
});
(0, vitest_1.it)('should update existing OpenCode config file', async () => {
const { clack, clackUtils } = await getMocks();
vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['openCode']);
vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
const existingConfig = JSON.stringify({
mcp: {
OtherServer: {
type: 'remote',
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.mcp).toHaveProperty('OtherServer');
(0, vitest_1.expect)(writtenContent.mcp).toHaveProperty('Sentry');
(0, vitest_1.expect)(writtenContent.mcp?.Sentry).toHaveProperty('type', 'remote');
(0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Updated opencode.json');
});
(0, vitest_1.it)('should handle file write errors gracefully for OpenCode', async () => {
const { clack, clackUtils } = await getMocks();
vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['openCode']);
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 for openCode'));
(0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
filename: 'opencode.json',
codeSnippet: vitest_1.expect.stringContaining('"mcp"'),
hint: 'create the file if it does not exist',
}));
});
(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');
vitest_1.vi.mocked(clack.multiselect).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'));
(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');
vitest_1.vi.mocked(clack.multiselect).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');
vitest_1.vi.mocked(clack.multiselect).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');
vitest_1.vi.mocked(clack.multiselect).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'));
(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');
vitest_1.vi.mocked(clack.multiselect).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'));
(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');
vitest_1.vi.mocked(clack.multiselect).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(true); // For the clipboard copy prompt
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['jetbrains']);
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(true); // For the clipboard copy prompt
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['other']);
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(true); // For clipboard copy prompt
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['jetbrains']);
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
vitest_1.vi.mocked(clack.multiselect).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(false); // User declines to copy to clipboard
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['jetbrains']);
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.multiselect).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should configure multiple editors when selected', async () => {
const { clack, clackUtils } = await getMocks();
vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce([
'cursor',
'vscode',
'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)();
// Should write config for all three editors
(0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledTimes(3);
(0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json'), vitest_1.expect.stringContaining('"mcpServers"'), 'utf8');
(0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.vscode/mcp.json'), vitest_1.expect.stringContaining('"servers"'), 'utf8');
(0, vitest_1.expect)(mockWriteFile).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.mcp.json'), vitest_1.expect.stringContaining('"mcpServers"'), 'utf8');
// Should show success messages for each (twice per editor: filename + editor-specific message)
(0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for Cursor.');
(0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for VS Code.');
(0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith('Added project-scoped Sentry MCP configuration for Claude Code.');
});
(0, vitest_1.it)('should return early when no editors are selected', async () => {
const { clack, clackUtils } = await getMocks();
vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce([]);
vitest_1.vi.mocked(clackUtils.abortIfCancelled).mockImplementation((value) => Promise.resolve(value));
const mockWriteFile = vitest_1.vi.fn();
vitest_1.vi.spyOn(fs.promises, 'writeFile').mockImplementation(mockWriteFile);
await (0, mcp_config_1.offerProjectScopedMcpConfig)();
(0, vitest_1.expect)(clack.log.info).toHaveBeenCalledWith('No editors selected. You can add MCP configuration later.');
(0, vitest_1.expect)(mockWriteFile).not.toHaveBeenCalled();
});
(0, vitest_1.it)('should handle mixed success and failure for multiple editors', async () => {
const { clack, clackUtils } = await getMocks();
vitest_1.vi.mocked(clack.select).mockResolvedValueOnce('yes');
vitest_1.vi.mocked(clack.multiselect).mockResolvedValueOnce(['cursor', '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()
.mockResolvedValueOnce(undefined) // Cursor succeeds
.mockRejectedValueOnce(new Error('Permission denied')); // VS Code fails
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)();
// Cursor should succeed
(0, vitest_1.expect)(clack.log.success).toHaveBeenCalledWith(vitest_1.expect.stringContaining('.cursor/mcp.json'));
// VS Code should fail and show fallback
(0, vitest_1.expect)(clack.log.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('Failed to write MCP config for vscode'));
(0, vitest_1.expect)(clackUtils.showCopyPasteInstructions).toHaveBeenCalledWith(vitest_1.expect.objectContaining({
filename: path.join('.vscode', 'mcp.json'),
}));
});
});
});
//# sourceMappingURL=mcp-config.test.js.map