From 0e49c06c4a14142404297f1dde213b7bf5fbd0bd Mon Sep 17 00:00:00 2001 From: Joe Danziger Date: Wed, 4 Jun 2025 21:14:14 -0400 Subject: [PATCH] add VS Code profile and tests --- scripts/profiles/index.js | 1 + scripts/profiles/vscode.js | 41 +++ src/constants/profiles.js | 4 +- src/utils/profiles.js | 23 +- .../profiles/mcp-config-validation.test.js | 37 ++- .../profiles/rule-transformer-vscode.test.js | 311 ++++++++++++++++++ tests/unit/profiles/rule-transformer.test.js | 24 +- .../unit/profiles/vscode-integration.test.js | 291 ++++++++++++++++ 8 files changed, 698 insertions(+), 34 deletions(-) create mode 100644 scripts/profiles/vscode.js create mode 100644 tests/unit/profiles/rule-transformer-vscode.test.js create mode 100644 tests/unit/profiles/vscode-integration.test.js diff --git a/scripts/profiles/index.js b/scripts/profiles/index.js index e1630dc5..9da3a933 100644 --- a/scripts/profiles/index.js +++ b/scripts/profiles/index.js @@ -5,4 +5,5 @@ export { codexProfile } from './codex.js'; export { cursorProfile } from './cursor.js'; export { rooProfile } from './roo.js'; export { traeProfile } from './trae.js'; +export { vscodeProfile } from './vscode.js'; export { windsurfProfile } from './windsurf.js'; diff --git a/scripts/profiles/vscode.js b/scripts/profiles/vscode.js new file mode 100644 index 00000000..49fd763e --- /dev/null +++ b/scripts/profiles/vscode.js @@ -0,0 +1,41 @@ +// VS Code conversion profile for rule-transformer +import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js'; + +// Create and export vscode profile using the base factory +export const vscodeProfile = createProfile({ + name: 'vscode', + displayName: 'VS Code', + url: 'code.visualstudio.com', + docsUrl: 'code.visualstudio.com/docs', + profileDir: '.vscode', // MCP config location + rulesDir: '.github/instructions', // VS Code instructions location + mcpConfig: true, + mcpConfigName: 'mcp.json', + fileExtension: '.mdc', + targetExtension: '.md', + toolMappings: COMMON_TOOL_MAPPINGS.STANDARD, // VS Code uses standard tool names + customFileMap: { + 'cursor_rules.mdc': 'vscode_rules.md' // Rename cursor_rules to vscode_rules + }, + customReplacements: [ + // Core VS Code directory structure changes + { from: /\.cursor\/rules/g, to: '.github/instructions' }, + { from: /\.cursor\/mcp\.json/g, to: '.vscode/mcp.json' }, + + // Fix any remaining vscode/rules references that might be created during transformation + { from: /\.vscode\/rules/g, to: '.github/instructions' }, + + // VS Code custom instructions format - use applyTo with quoted patterns instead of globs + { from: /^globs:\s*(.+)$/gm, to: 'applyTo: "$1"' }, + + // Essential markdown link transformations for VS Code structure + { + from: /\[(.+?)\]\(mdc:\.cursor\/rules\/(.+?)\.mdc\)/g, + to: '[$1](.github/instructions/$2.md)' + }, + + // VS Code specific terminology + { from: /rules directory/g, to: 'instructions directory' }, + { from: /cursor rules/gi, to: 'VS Code instructions' } + ] +}); diff --git a/src/constants/profiles.js b/src/constants/profiles.js index cf37332a..e4c4b8bf 100644 --- a/src/constants/profiles.js +++ b/src/constants/profiles.js @@ -1,5 +1,5 @@ /** - * @typedef {'claude' | 'cline' | 'codex' | 'cursor' | 'roo' | 'trae' | 'windsurf'} RulesProfile + * @typedef {'claude' | 'cline' | 'codex' | 'cursor' | 'roo' | 'trae' | 'windsurf' | 'vscode'} RulesProfile */ /** @@ -16,6 +16,7 @@ * - cursor: Cursor IDE rules * - roo: Roo Code IDE rules * - trae: Trae IDE rules + * - vscode: VS Code with GitHub Copilot integration * - windsurf: Windsurf IDE rules * * To add a new rule profile: @@ -30,6 +31,7 @@ export const RULE_PROFILES = [ 'cursor', 'roo', 'trae', + 'vscode', 'windsurf' ]; diff --git a/src/utils/profiles.js b/src/utils/profiles.js index 44ccf6bb..50e5558d 100644 --- a/src/utils/profiles.js +++ b/src/utils/profiles.js @@ -67,14 +67,8 @@ function getProfileDisplayName(name) { return profile?.displayName || name.charAt(0).toUpperCase() + name.slice(1); } -// Dynamically generate availableRulesProfiles from RULE_PROFILES -const availableRulesProfiles = RULE_PROFILES.map((name) => { - const displayName = getProfileDisplayName(name); - return { - name: displayName, - value: name - }; -}); +// Note: Profile choices are now generated dynamically within runInteractiveProfilesSetup() +// to ensure proper alphabetical sorting and pagination configuration /** * Launches an interactive prompt for selecting which rule profiles to include in your project. @@ -115,6 +109,7 @@ export async function runInteractiveProfilesSetup() { } return { + profileName, displayName, description }; @@ -142,11 +137,21 @@ export async function runInteractiveProfilesSetup() { ) ); + // Generate choices in the same order as the display text above + const sortedChoices = profileDescriptions.map( + ({ profileName, displayName }) => ({ + name: displayName, + value: profileName + }) + ); + const ruleProfilesQuestion = { type: 'checkbox', name: 'ruleProfiles', message: 'Which rule profiles would you like to add to your project?', - choices: availableRulesProfiles, + choices: sortedChoices, + pageSize: sortedChoices.length, // Show all options without pagination + loop: false, // Disable loop scrolling validate: (input) => input.length > 0 || 'You must select at least one.' }; const { ruleProfiles } = await inquirer.prompt([ruleProfilesQuestion]); diff --git a/tests/unit/profiles/mcp-config-validation.test.js b/tests/unit/profiles/mcp-config-validation.test.js index 6fb12b57..8ed2dda9 100644 --- a/tests/unit/profiles/mcp-config-validation.test.js +++ b/tests/unit/profiles/mcp-config-validation.test.js @@ -5,18 +5,18 @@ import path from 'path'; describe('MCP Configuration Validation', () => { describe('Profile MCP Configuration Properties', () => { const expectedMcpConfigurations = { + cline: { + shouldHaveMcp: false, + expectedDir: '.clinerules', + expectedConfigName: 'cline_mcp_settings.json', + expectedPath: '.clinerules/cline_mcp_settings.json' + }, cursor: { shouldHaveMcp: true, expectedDir: '.cursor', expectedConfigName: 'mcp.json', expectedPath: '.cursor/mcp.json' }, - windsurf: { - shouldHaveMcp: true, - expectedDir: '.windsurf', - expectedConfigName: 'mcp.json', - expectedPath: '.windsurf/mcp.json' - }, roo: { shouldHaveMcp: true, expectedDir: '.roo', @@ -29,11 +29,17 @@ describe('MCP Configuration Validation', () => { expectedConfigName: 'trae_mcp_settings.json', expectedPath: '.trae/trae_mcp_settings.json' }, - cline: { - shouldHaveMcp: false, - expectedDir: '.clinerules', - expectedConfigName: 'cline_mcp_settings.json', - expectedPath: '.clinerules/cline_mcp_settings.json' + vscode: { + shouldHaveMcp: true, + expectedDir: '.vscode', + expectedConfigName: 'mcp.json', + expectedPath: '.vscode/mcp.json' + }, + windsurf: { + shouldHaveMcp: true, + expectedDir: '.windsurf', + expectedConfigName: 'mcp.json', + expectedPath: '.windsurf/mcp.json' } }; @@ -98,7 +104,7 @@ describe('MCP Configuration Validation', () => { describe('MCP Configuration File Names', () => { test('should use standard mcp.json for MCP-enabled profiles', () => { - const standardMcpProfiles = ['cursor', 'windsurf', 'roo']; + const standardMcpProfiles = ['cursor', 'roo', 'vscode', 'windsurf']; standardMcpProfiles.forEach((profileName) => { const profile = getRulesProfile(profileName); expect(profile.mcpConfigName).toBe('mcp.json'); @@ -162,12 +168,13 @@ describe('MCP Configuration Validation', () => { }); expect(mcpEnabledProfiles).toContain('cursor'); - expect(mcpEnabledProfiles).toContain('windsurf'); expect(mcpEnabledProfiles).toContain('roo'); - expect(mcpEnabledProfiles).not.toContain('cline'); - expect(mcpEnabledProfiles).not.toContain('trae'); + expect(mcpEnabledProfiles).toContain('vscode'); + expect(mcpEnabledProfiles).toContain('windsurf'); expect(mcpEnabledProfiles).not.toContain('claude'); + expect(mcpEnabledProfiles).not.toContain('cline'); expect(mcpEnabledProfiles).not.toContain('codex'); + expect(mcpEnabledProfiles).not.toContain('trae'); }); test('should provide all necessary information for MCP config creation', () => { diff --git a/tests/unit/profiles/rule-transformer-vscode.test.js b/tests/unit/profiles/rule-transformer-vscode.test.js new file mode 100644 index 00000000..5e846523 --- /dev/null +++ b/tests/unit/profiles/rule-transformer-vscode.test.js @@ -0,0 +1,311 @@ +import { jest } from '@jest/globals'; + +// Mock fs module before importing anything that uses it +jest.mock('fs', () => ({ + readFileSync: jest.fn(), + writeFileSync: jest.fn(), + existsSync: jest.fn(), + mkdirSync: jest.fn() +})); + +// Import modules after mocking +import fs from 'fs'; +import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; +import { vscodeProfile } from '../../../scripts/profiles/vscode.js'; + +describe('VS Code Rule Transformer', () => { + // Set up spies on the mocked modules + const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); + const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); + const mockExistsSync = jest.spyOn(fs, 'existsSync'); + const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); + const mockConsoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + // Setup default mocks + mockReadFileSync.mockReturnValue(''); + mockWriteFileSync.mockImplementation(() => {}); + mockExistsSync.mockReturnValue(true); + mockMkdirSync.mockImplementation(() => {}); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should correctly convert basic terms', () => { + const testContent = `--- +description: Test Cursor rule for basic terms +globs: **/* +alwaysApply: true +--- + +This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. +Also has references to .mdc files and cursor rules.`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + vscodeProfile + ); + + // Verify the function succeeded + expect(result).toBe(true); + + // Verify file operations were called correctly + expect(mockReadFileSync).toHaveBeenCalledWith('source.mdc', 'utf8'); + expect(mockWriteFileSync).toHaveBeenCalledTimes(1); + + // Get the transformed content that was written + const writeCall = mockWriteFileSync.mock.calls[0]; + const transformedContent = writeCall[1]; + + // Verify transformations + expect(transformedContent).toContain('VS Code'); + expect(transformedContent).toContain('code.visualstudio.com'); + expect(transformedContent).toContain('.md'); + expect(transformedContent).toContain('vscode rules'); // "cursor rules" -> "vscode rules" + expect(transformedContent).toContain('applyTo: "**/*"'); // globs -> applyTo transformation + expect(transformedContent).not.toContain('cursor.so'); + expect(transformedContent).not.toContain('Cursor rule'); + expect(transformedContent).not.toContain('globs:'); + }); + + it('should correctly convert tool references', () => { + const testContent = `--- +description: Test Cursor rule for tool references +globs: **/* +alwaysApply: true +--- + +- Use the search tool to find code +- The edit_file tool lets you modify files +- run_command executes terminal commands +- use_mcp connects to external services`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + vscodeProfile + ); + + // Verify the function succeeded + expect(result).toBe(true); + + // Get the transformed content that was written + const writeCall = mockWriteFileSync.mock.calls[0]; + const transformedContent = writeCall[1]; + + // Verify transformations (VS Code uses standard tool names, so no transformation) + expect(transformedContent).toContain('search tool'); + expect(transformedContent).toContain('edit_file tool'); + expect(transformedContent).toContain('run_command'); + expect(transformedContent).toContain('use_mcp'); + expect(transformedContent).toContain('applyTo: "**/*"'); // globs -> applyTo transformation + }); + + it('should correctly update file references and directory paths', () => { + const testContent = `--- +description: Test Cursor rule for file references +globs: .cursor/rules/*.md +alwaysApply: true +--- + +This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and +[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc). +Files are in the .cursor/rules directory and we should reference the rules directory.`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + vscodeProfile + ); + + // Verify the function succeeded + expect(result).toBe(true); + + // Get the transformed content that was written + const writeCall = mockWriteFileSync.mock.calls[0]; + const transformedContent = writeCall[1]; + + // Verify transformations specific to VS Code + expect(transformedContent).toContain( + 'applyTo: ".github/instructions/*.md"' + ); // globs -> applyTo with path transformation + expect(transformedContent).toContain( + '(.github/instructions/taskmaster/dev_workflow.md)' + ); // File path transformation + expect(transformedContent).toContain( + '(.github/instructions/taskmaster/taskmaster.md)' + ); // File path transformation + expect(transformedContent).toContain('instructions directory'); // "rules directory" -> "instructions directory" + expect(transformedContent).not.toContain('(mdc:.cursor/rules/'); + expect(transformedContent).not.toContain('.cursor/rules'); + expect(transformedContent).not.toContain('globs:'); + expect(transformedContent).not.toContain('rules directory'); + }); + + it('should transform globs to applyTo with various patterns', () => { + const testContent = `--- +description: Test VS Code applyTo transformation +globs: .cursor/rules/*.md +alwaysApply: true +--- + +Another section: +globs: **/*.ts +final: true + +Last one: +globs: src/**/* +---`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + vscodeProfile + ); + + // Verify the function succeeded + expect(result).toBe(true); + + // Get the transformed content that was written + const writeCall = mockWriteFileSync.mock.calls[0]; + const transformedContent = writeCall[1]; + + // Verify all globs transformations + expect(transformedContent).toContain( + 'applyTo: ".github/instructions/*.md"' + ); // Path transformation applied + expect(transformedContent).toContain('applyTo: "**/*.ts"'); // Pattern with quotes + expect(transformedContent).toContain('applyTo: "src/**/*"'); // Complex pattern with quotes + expect(transformedContent).not.toContain('globs:'); // No globs should remain + }); + + it('should handle VS Code MCP configuration paths correctly', () => { + const testContent = `--- +description: Test MCP configuration paths +globs: **/* +alwaysApply: true +--- + +MCP configuration is at .cursor/mcp.json for Cursor. +The .cursor/rules directory contains rules. +Update your .cursor/mcp.json file accordingly.`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + vscodeProfile + ); + + // Verify the function succeeded + expect(result).toBe(true); + + // Get the transformed content that was written + const writeCall = mockWriteFileSync.mock.calls[0]; + const transformedContent = writeCall[1]; + + // Verify MCP paths are correctly transformed + expect(transformedContent).toContain('.vscode/mcp.json'); // MCP config in .vscode + expect(transformedContent).toContain('.github/instructions'); // Rules/instructions in .github/instructions + expect(transformedContent).not.toContain('.cursor/mcp.json'); + expect(transformedContent).not.toContain('.cursor/rules'); + }); + + it('should handle file read errors', () => { + // Mock file read to throw an error + mockReadFileSync.mockImplementation(() => { + throw new Error('File not found'); + }); + + // Call the actual function + const result = convertRuleToProfileRule( + 'nonexistent.mdc', + 'target.md', + vscodeProfile + ); + + // Verify the function failed gracefully + expect(result).toBe(false); + + // Verify writeFileSync was not called + expect(mockWriteFileSync).not.toHaveBeenCalled(); + + // Verify error was logged + expect(mockConsoleError).toHaveBeenCalledWith( + 'Error converting rule file: File not found' + ); + }); + + it('should handle file write errors', () => { + const testContent = 'test content'; + mockReadFileSync.mockReturnValue(testContent); + + // Mock file write to throw an error + mockWriteFileSync.mockImplementation(() => { + throw new Error('Permission denied'); + }); + + // Call the actual function + const result = convertRuleToProfileRule( + 'source.mdc', + 'target.md', + vscodeProfile + ); + + // Verify the function failed gracefully + expect(result).toBe(false); + + // Verify error was logged + expect(mockConsoleError).toHaveBeenCalledWith( + 'Error converting rule file: Permission denied' + ); + }); + + it('should create target directory if it does not exist', () => { + const testContent = 'test content'; + mockReadFileSync.mockReturnValue(testContent); + + // Mock directory doesn't exist initially + mockExistsSync.mockReturnValue(false); + + // Call the actual function + convertRuleToProfileRule( + 'source.mdc', + '.github/instructions/deep/path/target.md', + vscodeProfile + ); + + // Verify directory creation was called + expect(mockMkdirSync).toHaveBeenCalledWith( + '.github/instructions/deep/path', + { + recursive: true + } + ); + }); +}); diff --git a/tests/unit/profiles/rule-transformer.test.js b/tests/unit/profiles/rule-transformer.test.js index 8ebd29fb..33d812d2 100644 --- a/tests/unit/profiles/rule-transformer.test.js +++ b/tests/unit/profiles/rule-transformer.test.js @@ -19,6 +19,7 @@ describe('Rule Transformer - General', () => { 'cursor', 'roo', 'trae', + 'vscode', 'windsurf' ]; expectedProfiles.forEach((profile) => { @@ -181,6 +182,11 @@ describe('Rule Transformer - General', () => { mcpConfigName: null, expectedPath: null }, + cline: { + mcpConfig: false, + mcpConfigName: 'cline_mcp_settings.json', + expectedPath: '.clinerules/cline_mcp_settings.json' + }, codex: { mcpConfig: false, mcpConfigName: null, @@ -191,11 +197,6 @@ describe('Rule Transformer - General', () => { mcpConfigName: 'mcp.json', expectedPath: '.cursor/mcp.json' }, - windsurf: { - mcpConfig: true, - mcpConfigName: 'mcp.json', - expectedPath: '.windsurf/mcp.json' - }, roo: { mcpConfig: true, mcpConfigName: 'mcp.json', @@ -206,10 +207,15 @@ describe('Rule Transformer - General', () => { mcpConfigName: 'trae_mcp_settings.json', expectedPath: '.trae/trae_mcp_settings.json' }, - cline: { - mcpConfig: false, - mcpConfigName: 'cline_mcp_settings.json', - expectedPath: '.clinerules/cline_mcp_settings.json' + vscode: { + mcpConfig: true, + mcpConfigName: 'mcp.json', + expectedPath: '.vscode/mcp.json' + }, + windsurf: { + mcpConfig: true, + mcpConfigName: 'mcp.json', + expectedPath: '.windsurf/mcp.json' } }; diff --git a/tests/unit/profiles/vscode-integration.test.js b/tests/unit/profiles/vscode-integration.test.js new file mode 100644 index 00000000..6dece51b --- /dev/null +++ b/tests/unit/profiles/vscode-integration.test.js @@ -0,0 +1,291 @@ +import { jest } from '@jest/globals'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +// Mock external modules +jest.mock('child_process', () => ({ + execSync: jest.fn() +})); + +// Mock console methods +jest.mock('console', () => ({ + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + clear: jest.fn() +})); + +describe('VS Code Integration', () => { + let tempDir; + + beforeEach(() => { + jest.clearAllMocks(); + + // Create a temporary directory for testing + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); + + // Spy on fs methods + jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { + if (filePath.toString().includes('mcp.json')) { + return JSON.stringify({ + mcpServers: { + 'task-master-ai': { + command: 'node', + args: ['mcp-server/src/index.js'] + } + } + }); + } + if (filePath.toString().includes('instructions')) { + return 'VS Code instruction content'; + } + return '{}'; + }); + jest.spyOn(fs, 'existsSync').mockImplementation(() => false); + jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); + }); + + afterEach(() => { + // Clean up the temporary directory + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (err) { + console.error(`Error cleaning up: ${err.message}`); + } + }); + + // Test function that simulates the createProjectStructure behavior for VS Code files + function mockCreateVSCodeStructure() { + // Create .vscode directory for MCP configuration + fs.mkdirSync(path.join(tempDir, '.vscode'), { recursive: true }); + + // Create .github/instructions directory for VS Code custom instructions + fs.mkdirSync(path.join(tempDir, '.github', 'instructions'), { + recursive: true + }); + fs.mkdirSync(path.join(tempDir, '.github', 'instructions', 'taskmaster'), { + recursive: true + }); + + // Create MCP configuration file + const mcpConfig = { + mcpServers: { + 'task-master-ai': { + command: 'node', + args: ['mcp-server/src/index.js'], + env: { + PROJECT_ROOT: process.cwd() + } + } + } + }; + fs.writeFileSync( + path.join(tempDir, '.vscode', 'mcp.json'), + JSON.stringify(mcpConfig, null, 2) + ); + + // Create sample instruction files + const instructionFiles = [ + 'vscode_rules.md', + 'dev_workflow.md', + 'self_improve.md' + ]; + + for (const file of instructionFiles) { + const content = `--- +description: VS Code instruction for ${file} +applyTo: "**/*.ts,**/*.tsx,**/*.js,**/*.jsx" +alwaysApply: true +--- + +# ${file.replace('.md', '').replace('_', ' ').toUpperCase()} + +This is a VS Code custom instruction file.`; + + fs.writeFileSync( + path.join(tempDir, '.github', 'instructions', file), + content + ); + } + + // Create taskmaster subdirectory with additional instructions + const taskmasterFiles = ['taskmaster.md', 'commands.md', 'architecture.md']; + + for (const file of taskmasterFiles) { + const content = `--- +description: Task Master specific instruction for ${file} +applyTo: "**/*.ts,**/*.js" +alwaysApply: true +--- + +# ${file.replace('.md', '').toUpperCase()} + +Task Master specific VS Code instruction.`; + + fs.writeFileSync( + path.join(tempDir, '.github', 'instructions', 'taskmaster', file), + content + ); + } + } + + test('creates all required VS Code directories', () => { + // Act + mockCreateVSCodeStructure(); + + // Assert - .vscode directory for MCP config + expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.vscode'), { + recursive: true + }); + + // Assert - .github/instructions directory for custom instructions + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.github', 'instructions'), + { recursive: true } + ); + + // Assert - taskmaster subdirectory + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.github', 'instructions', 'taskmaster'), + { recursive: true } + ); + }); + + test('creates VS Code MCP configuration file', () => { + // Act + mockCreateVSCodeStructure(); + + // Assert + const expectedMcpPath = path.join(tempDir, '.vscode', 'mcp.json'); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expectedMcpPath, + expect.stringContaining('task-master-ai') + ); + }); + + test('creates VS Code instruction files with applyTo patterns', () => { + // Act + mockCreateVSCodeStructure(); + + // Assert main instruction files + const mainInstructionFiles = [ + 'vscode_rules.md', + 'dev_workflow.md', + 'self_improve.md' + ]; + + for (const file of mainInstructionFiles) { + const expectedPath = path.join(tempDir, '.github', 'instructions', file); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expectedPath, + expect.stringContaining('applyTo:') + ); + } + }); + + test('creates taskmaster specific instruction files', () => { + // Act + mockCreateVSCodeStructure(); + + // Assert taskmaster subdirectory files + const taskmasterFiles = ['taskmaster.md', 'commands.md', 'architecture.md']; + + for (const file of taskmasterFiles) { + const expectedPath = path.join( + tempDir, + '.github', + 'instructions', + 'taskmaster', + file + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + expectedPath, + expect.stringContaining('applyTo:') + ); + } + }); + + test('VS Code instruction files use applyTo instead of globs', () => { + // Act + mockCreateVSCodeStructure(); + + // Get all the writeFileSync calls for .md files + const mdFileWrites = fs.writeFileSync.mock.calls.filter((call) => + call[0].toString().endsWith('.md') + ); + + // Assert that all .md files contain applyTo and not globs + for (const writeCall of mdFileWrites) { + const content = writeCall[1]; + expect(content).toContain('applyTo:'); + expect(content).not.toContain('globs:'); + } + }); + + test('MCP configuration includes correct structure for VS Code', () => { + // Act + mockCreateVSCodeStructure(); + + // Get the MCP config write call + const mcpConfigWrite = fs.writeFileSync.mock.calls.find((call) => + call[0].toString().includes('mcp.json') + ); + + expect(mcpConfigWrite).toBeDefined(); + + const mcpContent = mcpConfigWrite[1]; + const mcpConfig = JSON.parse(mcpContent); + + // Assert MCP structure + expect(mcpConfig).toHaveProperty('mcpServers'); + expect(mcpConfig.mcpServers).toHaveProperty('task-master-ai'); + expect(mcpConfig.mcpServers['task-master-ai']).toHaveProperty( + 'command', + 'node' + ); + expect(mcpConfig.mcpServers['task-master-ai']).toHaveProperty('args'); + expect(mcpConfig.mcpServers['task-master-ai'].args).toContain( + 'mcp-server/src/index.js' + ); + }); + + test('directory structure follows VS Code conventions', () => { + // Act + mockCreateVSCodeStructure(); + + // Assert the specific directory structure VS Code expects + const expectedDirs = [ + path.join(tempDir, '.vscode'), + path.join(tempDir, '.github', 'instructions'), + path.join(tempDir, '.github', 'instructions', 'taskmaster') + ]; + + for (const dir of expectedDirs) { + expect(fs.mkdirSync).toHaveBeenCalledWith(dir, { recursive: true }); + } + }); + + test('instruction files contain VS Code specific formatting', () => { + // Act + mockCreateVSCodeStructure(); + + // Get a sample instruction file write + const instructionWrite = fs.writeFileSync.mock.calls.find((call) => + call[0].toString().includes('vscode_rules.md') + ); + + expect(instructionWrite).toBeDefined(); + + const content = instructionWrite[1]; + + // Assert VS Code specific patterns + expect(content).toContain('---'); // YAML frontmatter + expect(content).toContain('description:'); + expect(content).toContain('applyTo:'); + expect(content).toContain('alwaysApply:'); + expect(content).toContain('**/*.ts'); // File patterns in quotes + }); +});