@sentry/wizard
Version:
Sentry wizard helping you to configure your project
520 lines • 32.7 kB
JavaScript
;
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