add VS Code profile and tests

This commit is contained in:
Joe Danziger
2025-06-04 21:14:14 -04:00
parent 18e52c2012
commit 0e49c06c4a
8 changed files with 698 additions and 34 deletions

View File

@@ -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';

View File

@@ -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' }
]
});

View File

@@ -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'
];

View File

@@ -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]);

View File

@@ -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', () => {

View File

@@ -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
}
);
});
});

View File

@@ -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'
}
};

View File

@@ -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
});
});