organize tests into profiles folder
This commit is contained in:
103
tests/unit/profiles/claude-integration.test.js
Normal file
103
tests/unit/profiles/claude-integration.test.js
Normal file
@@ -0,0 +1,103 @@
|
||||
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('Claude Profile 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('AGENTS.md')) {
|
||||
return 'Sample AGENTS.md content for Claude integration';
|
||||
}
|
||||
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 Claude profile file copying behavior
|
||||
function mockCreateClaudeStructure() {
|
||||
// Claude profile copies AGENTS.md to CLAUDE.md in project root
|
||||
const sourceContent = 'Sample AGENTS.md content for Claude integration';
|
||||
fs.writeFileSync(path.join(tempDir, 'CLAUDE.md'), sourceContent);
|
||||
}
|
||||
|
||||
test('creates CLAUDE.md file in project root', () => {
|
||||
// Act
|
||||
mockCreateClaudeStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, 'CLAUDE.md'),
|
||||
'Sample AGENTS.md content for Claude integration'
|
||||
);
|
||||
});
|
||||
|
||||
test('does not create any profile directories', () => {
|
||||
// Act
|
||||
mockCreateClaudeStructure();
|
||||
|
||||
// Assert - Claude profile should not create any directories
|
||||
// Only the temp directory creation calls should exist
|
||||
const mkdirCalls = fs.mkdirSync.mock.calls.filter(
|
||||
(call) => !call[0].includes('task-master-test-')
|
||||
);
|
||||
expect(mkdirCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not create MCP configuration files', () => {
|
||||
// Act
|
||||
mockCreateClaudeStructure();
|
||||
|
||||
// Assert - Claude profile should not create any MCP config files
|
||||
const writeFileCalls = fs.writeFileSync.mock.calls;
|
||||
const mcpConfigCalls = writeFileCalls.filter(
|
||||
(call) =>
|
||||
call[0].toString().includes('mcp.json') ||
|
||||
call[0].toString().includes('mcp_settings.json')
|
||||
);
|
||||
expect(mcpConfigCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('only creates the target integration guide file', () => {
|
||||
// Act
|
||||
mockCreateClaudeStructure();
|
||||
|
||||
// Assert - Should only create CLAUDE.md
|
||||
const writeFileCalls = fs.writeFileSync.mock.calls;
|
||||
expect(writeFileCalls).toHaveLength(1);
|
||||
expect(writeFileCalls[0][0]).toBe(path.join(tempDir, 'CLAUDE.md'));
|
||||
});
|
||||
});
|
||||
113
tests/unit/profiles/codex-integration.test.js
Normal file
113
tests/unit/profiles/codex-integration.test.js
Normal file
@@ -0,0 +1,113 @@
|
||||
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('Codex Profile 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('AGENTS.md')) {
|
||||
return 'Sample AGENTS.md content for Codex integration';
|
||||
}
|
||||
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 Codex profile file copying behavior
|
||||
function mockCreateCodexStructure() {
|
||||
// Codex profile copies AGENTS.md to AGENTS.md in project root (same name)
|
||||
const sourceContent = 'Sample AGENTS.md content for Codex integration';
|
||||
fs.writeFileSync(path.join(tempDir, 'AGENTS.md'), sourceContent);
|
||||
}
|
||||
|
||||
test('creates AGENTS.md file in project root', () => {
|
||||
// Act
|
||||
mockCreateCodexStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, 'AGENTS.md'),
|
||||
'Sample AGENTS.md content for Codex integration'
|
||||
);
|
||||
});
|
||||
|
||||
test('does not create any profile directories', () => {
|
||||
// Act
|
||||
mockCreateCodexStructure();
|
||||
|
||||
// Assert - Codex profile should not create any directories
|
||||
// Only the temp directory creation calls should exist
|
||||
const mkdirCalls = fs.mkdirSync.mock.calls.filter(
|
||||
(call) => !call[0].includes('task-master-test-')
|
||||
);
|
||||
expect(mkdirCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('does not create MCP configuration files', () => {
|
||||
// Act
|
||||
mockCreateCodexStructure();
|
||||
|
||||
// Assert - Codex profile should not create any MCP config files
|
||||
const writeFileCalls = fs.writeFileSync.mock.calls;
|
||||
const mcpConfigCalls = writeFileCalls.filter(
|
||||
(call) =>
|
||||
call[0].toString().includes('mcp.json') ||
|
||||
call[0].toString().includes('mcp_settings.json')
|
||||
);
|
||||
expect(mcpConfigCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('only creates the target integration guide file', () => {
|
||||
// Act
|
||||
mockCreateCodexStructure();
|
||||
|
||||
// Assert - Should only create AGENTS.md
|
||||
const writeFileCalls = fs.writeFileSync.mock.calls;
|
||||
expect(writeFileCalls).toHaveLength(1);
|
||||
expect(writeFileCalls[0][0]).toBe(path.join(tempDir, 'AGENTS.md'));
|
||||
});
|
||||
|
||||
test('uses the same filename as source (AGENTS.md)', () => {
|
||||
// Act
|
||||
mockCreateCodexStructure();
|
||||
|
||||
// Assert - Codex should keep the same filename unlike Claude which renames it
|
||||
const writeFileCalls = fs.writeFileSync.mock.calls;
|
||||
expect(writeFileCalls[0][0]).toContain('AGENTS.md');
|
||||
expect(writeFileCalls[0][0]).not.toContain('CLAUDE.md');
|
||||
});
|
||||
});
|
||||
100
tests/unit/profiles/cursor-integration.test.js
Normal file
100
tests/unit/profiles/cursor-integration.test.js
Normal file
@@ -0,0 +1,100 @@
|
||||
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('Cursor 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('.cursormodes')) {
|
||||
return 'Existing cursormodes content';
|
||||
}
|
||||
if (filePath.toString().includes('-rules')) {
|
||||
return 'Existing mode rules 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 Cursor files
|
||||
function mockCreateCursorStructure() {
|
||||
// Create main .cursor directory
|
||||
fs.mkdirSync(path.join(tempDir, '.cursor'), { recursive: true });
|
||||
|
||||
// Create rules directory
|
||||
fs.mkdirSync(path.join(tempDir, '.cursor', 'rules'), { recursive: true });
|
||||
|
||||
// Create mode-specific rule directories
|
||||
const cursorModes = [
|
||||
'architect',
|
||||
'ask',
|
||||
'boomerang',
|
||||
'code',
|
||||
'debug',
|
||||
'test'
|
||||
];
|
||||
for (const mode of cursorModes) {
|
||||
fs.mkdirSync(path.join(tempDir, '.cursor', `rules-${mode}`), {
|
||||
recursive: true
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.cursor', `rules-${mode}`, `${mode}-rules`),
|
||||
`Content for ${mode} rules`
|
||||
);
|
||||
}
|
||||
|
||||
// Copy .cursormodes file
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.cursormodes'),
|
||||
'Cursormodes file content'
|
||||
);
|
||||
}
|
||||
|
||||
test('creates all required .cursor directories', () => {
|
||||
// Act
|
||||
mockCreateCursorStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.cursor'), {
|
||||
recursive: true
|
||||
});
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.cursor', 'rules'),
|
||||
{ recursive: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
240
tests/unit/profiles/mcp-config-validation.test.js
Normal file
240
tests/unit/profiles/mcp-config-validation.test.js
Normal file
@@ -0,0 +1,240 @@
|
||||
import { RULE_PROFILES } from '../../../src/constants/profiles.js';
|
||||
import { getRulesProfile } from '../../../src/utils/rule-transformer.js';
|
||||
import path from 'path';
|
||||
|
||||
describe('MCP Configuration Validation', () => {
|
||||
describe('Profile MCP Configuration Properties', () => {
|
||||
const expectedMcpConfigurations = {
|
||||
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',
|
||||
expectedConfigName: 'mcp.json',
|
||||
expectedPath: '.roo/mcp.json'
|
||||
},
|
||||
trae: {
|
||||
shouldHaveMcp: false,
|
||||
expectedDir: '.trae',
|
||||
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'
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(expectedMcpConfigurations).forEach(
|
||||
([profileName, expected]) => {
|
||||
test(`should have correct MCP configuration for ${profileName} profile`, () => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
expect(profile).toBeDefined();
|
||||
expect(profile.mcpConfig).toBe(expected.shouldHaveMcp);
|
||||
expect(profile.profileDir).toBe(expected.expectedDir);
|
||||
expect(profile.mcpConfigName).toBe(expected.expectedConfigName);
|
||||
expect(profile.mcpConfigPath).toBe(expected.expectedPath);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('MCP Configuration Path Consistency', () => {
|
||||
test('should ensure all profiles have consistent mcpConfigPath construction', () => {
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
const expectedPath = path.join(
|
||||
profile.profileDir,
|
||||
profile.mcpConfigName
|
||||
);
|
||||
expect(profile.mcpConfigPath).toBe(expectedPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should ensure no two profiles have the same MCP config path', () => {
|
||||
const mcpPaths = new Set();
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
expect(mcpPaths.has(profile.mcpConfigPath)).toBe(false);
|
||||
mcpPaths.add(profile.mcpConfigPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should ensure all MCP-enabled profiles use proper directory structure', () => {
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should ensure all profiles have required MCP properties', () => {
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
expect(profile).toHaveProperty('mcpConfig');
|
||||
expect(profile).toHaveProperty('profileDir');
|
||||
expect(profile).toHaveProperty('mcpConfigName');
|
||||
expect(profile).toHaveProperty('mcpConfigPath');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Configuration File Names', () => {
|
||||
test('should use standard mcp.json for MCP-enabled profiles', () => {
|
||||
const standardMcpProfiles = ['cursor', 'windsurf', 'roo'];
|
||||
standardMcpProfiles.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
expect(profile.mcpConfigName).toBe('mcp.json');
|
||||
});
|
||||
});
|
||||
|
||||
test('should use profile-specific config name for non-MCP profiles', () => {
|
||||
const clineProfile = getRulesProfile('cline');
|
||||
expect(clineProfile.mcpConfigName).toBe('cline_mcp_settings.json');
|
||||
|
||||
const traeProfile = getRulesProfile('trae');
|
||||
expect(traeProfile.mcpConfigName).toBe('trae_mcp_settings.json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Profile Directory Structure', () => {
|
||||
test('should ensure each profile has a unique directory', () => {
|
||||
const profileDirs = new Set();
|
||||
// Simple profiles that use root directory (can share the same directory)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
|
||||
// Simple profiles can share the root directory
|
||||
if (simpleProfiles.includes(profileName)) {
|
||||
expect(profile.profileDir).toBe('.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Full profiles should have unique directories
|
||||
expect(profileDirs.has(profile.profileDir)).toBe(false);
|
||||
profileDirs.add(profile.profileDir);
|
||||
});
|
||||
});
|
||||
|
||||
test('should ensure profile directories follow expected naming convention', () => {
|
||||
// Simple profiles that use root directory
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
|
||||
// Simple profiles use root directory
|
||||
if (simpleProfiles.includes(profileName)) {
|
||||
expect(profile.profileDir).toBe('.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Full profiles should follow the .name pattern
|
||||
expect(profile.profileDir).toMatch(/^\.[\w-]+$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Configuration Creation Logic', () => {
|
||||
test('should indicate which profiles require MCP configuration creation', () => {
|
||||
const mcpEnabledProfiles = RULE_PROFILES.filter((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
return profile.mcpConfig !== false;
|
||||
});
|
||||
|
||||
expect(mcpEnabledProfiles).toContain('cursor');
|
||||
expect(mcpEnabledProfiles).toContain('windsurf');
|
||||
expect(mcpEnabledProfiles).toContain('roo');
|
||||
expect(mcpEnabledProfiles).not.toContain('cline');
|
||||
expect(mcpEnabledProfiles).not.toContain('trae');
|
||||
expect(mcpEnabledProfiles).not.toContain('claude');
|
||||
expect(mcpEnabledProfiles).not.toContain('codex');
|
||||
});
|
||||
|
||||
test('should provide all necessary information for MCP config creation', () => {
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
expect(profile.mcpConfigPath).toBeDefined();
|
||||
expect(typeof profile.mcpConfigPath).toBe('string');
|
||||
expect(profile.mcpConfigPath.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Configuration Path Usage Verification', () => {
|
||||
test('should verify that rule transformer functions use mcpConfigPath correctly', () => {
|
||||
// This test verifies that the mcpConfigPath property exists and is properly formatted
|
||||
// for use with the setupMCPConfiguration function
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
// Verify the path is properly formatted for path.join usage
|
||||
expect(profile.mcpConfigPath.startsWith('/')).toBe(false);
|
||||
expect(profile.mcpConfigPath).toContain('/');
|
||||
|
||||
// Verify it matches the expected pattern: profileDir/configName
|
||||
const expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
|
||||
expect(profile.mcpConfigPath).toBe(expectedPath);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('should verify that mcpConfigPath is properly constructed for path.join usage', () => {
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
// Test that path.join works correctly with the mcpConfigPath
|
||||
const testProjectRoot = '/test/project';
|
||||
const fullPath = path.join(testProjectRoot, profile.mcpConfigPath);
|
||||
|
||||
// Should result in a proper absolute path
|
||||
expect(fullPath).toBe(`${testProjectRoot}/${profile.mcpConfigPath}`);
|
||||
expect(fullPath).toContain(profile.profileDir);
|
||||
expect(fullPath).toContain(profile.mcpConfigName);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Configuration Function Integration', () => {
|
||||
test('should verify that setupMCPConfiguration receives the correct mcpConfigPath parameter', () => {
|
||||
// This test verifies the integration between rule transformer and mcp-utils
|
||||
RULE_PROFILES.forEach((profileName) => {
|
||||
const profile = getRulesProfile(profileName);
|
||||
if (profile.mcpConfig !== false) {
|
||||
// Verify that the mcpConfigPath can be used directly with setupMCPConfiguration
|
||||
// The function signature is: setupMCPConfiguration(projectDir, mcpConfigPath)
|
||||
expect(profile.mcpConfigPath).toBeDefined();
|
||||
expect(typeof profile.mcpConfigPath).toBe('string');
|
||||
|
||||
// Verify the path structure is correct for the new function signature
|
||||
const parts = profile.mcpConfigPath.split('/');
|
||||
expect(parts).toHaveLength(2); // Should be profileDir/configName
|
||||
expect(parts[0]).toBe(profile.profileDir);
|
||||
expect(parts[1]).toBe(profile.mcpConfigName);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
175
tests/unit/profiles/profile-safety-check.test.js
Normal file
175
tests/unit/profiles/profile-safety-check.test.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import {
|
||||
getInstalledProfiles,
|
||||
wouldRemovalLeaveNoProfiles
|
||||
} from '../../../src/utils/profiles.js';
|
||||
import { rulesDirect } from '../../../mcp-server/src/core/direct-functions/rules.js';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock logger
|
||||
const mockLog = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn()
|
||||
};
|
||||
|
||||
describe('Rules Safety Check', () => {
|
||||
let mockExistsSync;
|
||||
let mockRmSync;
|
||||
let mockReaddirSync;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Set up spies on fs methods
|
||||
mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
mockRmSync = jest.spyOn(fs, 'rmSync').mockImplementation(() => {});
|
||||
mockReaddirSync = jest.spyOn(fs, 'readdirSync').mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore all mocked functions
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getInstalledProfiles', () => {
|
||||
it('should detect installed profiles correctly', () => {
|
||||
const projectRoot = '/test/project';
|
||||
|
||||
// Mock fs.existsSync to simulate installed profiles
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.cursor') || filePath.includes('.roo')) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const installed = getInstalledProfiles(projectRoot);
|
||||
expect(installed).toContain('cursor');
|
||||
expect(installed).toContain('roo');
|
||||
expect(installed).not.toContain('windsurf');
|
||||
expect(installed).not.toContain('cline');
|
||||
});
|
||||
|
||||
it('should return empty array when no profiles are installed', () => {
|
||||
const projectRoot = '/test/project';
|
||||
|
||||
// Mock fs.existsSync to return false for all paths
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const installed = getInstalledProfiles(projectRoot);
|
||||
expect(installed).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('wouldRemovalLeaveNoProfiles', () => {
|
||||
it('should return true when removing all installed profiles', () => {
|
||||
const projectRoot = '/test/project';
|
||||
|
||||
// Mock fs.existsSync to simulate cursor and roo installed
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
return filePath.includes('.cursor') || filePath.includes('.roo');
|
||||
});
|
||||
|
||||
const result = wouldRemovalLeaveNoProfiles(projectRoot, [
|
||||
'cursor',
|
||||
'roo'
|
||||
]);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when removing only some profiles', () => {
|
||||
const projectRoot = '/test/project';
|
||||
|
||||
// Mock fs.existsSync to simulate cursor and roo installed
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
return filePath.includes('.cursor') || filePath.includes('.roo');
|
||||
});
|
||||
|
||||
const result = wouldRemovalLeaveNoProfiles(projectRoot, ['roo']);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when no profiles are currently installed', () => {
|
||||
const projectRoot = '/test/project';
|
||||
|
||||
// Mock fs.existsSync to return false for all paths
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = wouldRemovalLeaveNoProfiles(projectRoot, ['cursor']);
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Safety Check Integration', () => {
|
||||
it('should block removal of all profiles without force', async () => {
|
||||
const projectRoot = '/test/project';
|
||||
|
||||
// Mock fs.existsSync to simulate installed profiles
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
return filePath.includes('.cursor') || filePath.includes('.roo');
|
||||
});
|
||||
|
||||
const result = await rulesDirect(
|
||||
{
|
||||
action: 'remove',
|
||||
profiles: ['cursor', 'roo'],
|
||||
projectRoot,
|
||||
force: false
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error.code).toBe('CRITICAL_REMOVAL_BLOCKED');
|
||||
expect(result.error.message).toContain('CRITICAL');
|
||||
});
|
||||
|
||||
it('should allow removal of all profiles with force', async () => {
|
||||
const projectRoot = '/test/project';
|
||||
|
||||
// Mock fs.existsSync and other file operations for successful removal
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
|
||||
const result = await rulesDirect(
|
||||
{
|
||||
action: 'remove',
|
||||
profiles: ['cursor', 'roo'],
|
||||
projectRoot,
|
||||
force: true
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow partial removal without force', async () => {
|
||||
const projectRoot = '/test/project';
|
||||
|
||||
// Mock fs.existsSync to simulate multiple profiles installed
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
return (
|
||||
filePath.includes('.cursor') ||
|
||||
filePath.includes('.roo') ||
|
||||
filePath.includes('.windsurf')
|
||||
);
|
||||
});
|
||||
|
||||
const result = await rulesDirect(
|
||||
{
|
||||
action: 'remove',
|
||||
profiles: ['roo'], // Only removing one profile
|
||||
projectRoot,
|
||||
force: false
|
||||
},
|
||||
mockLog
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
182
tests/unit/profiles/roo-integration.test.js
Normal file
182
tests/unit/profiles/roo-integration.test.js
Normal file
@@ -0,0 +1,182 @@
|
||||
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('Roo 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('.roomodes')) {
|
||||
return 'Existing roomodes content';
|
||||
}
|
||||
if (filePath.toString().includes('-rules')) {
|
||||
return 'Existing mode rules 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 Roo files
|
||||
function mockCreateRooStructure() {
|
||||
// Create main .roo directory
|
||||
fs.mkdirSync(path.join(tempDir, '.roo'), { recursive: true });
|
||||
|
||||
// Create rules directory
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', 'rules'), { recursive: true });
|
||||
|
||||
// Create mode-specific rule directories
|
||||
const rooModes = ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'];
|
||||
for (const mode of rooModes) {
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', `rules-${mode}`), {
|
||||
recursive: true
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.roo', `rules-${mode}`, `${mode}-rules`),
|
||||
`Content for ${mode} rules`
|
||||
);
|
||||
}
|
||||
|
||||
// Create additional directories
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', 'config'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', 'templates'), { recursive: true });
|
||||
fs.mkdirSync(path.join(tempDir, '.roo', 'logs'), { recursive: true });
|
||||
|
||||
// Copy .roomodes file
|
||||
fs.writeFileSync(path.join(tempDir, '.roomodes'), 'Roomodes file content');
|
||||
}
|
||||
|
||||
test('creates all required .roo directories', () => {
|
||||
// Act
|
||||
mockCreateRooStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.roo'), {
|
||||
recursive: true
|
||||
});
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules'),
|
||||
{ recursive: true }
|
||||
);
|
||||
|
||||
// Verify all mode directories are created
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-architect'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-ask'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-boomerang'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-code'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-debug'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-test'),
|
||||
{ recursive: true }
|
||||
);
|
||||
});
|
||||
|
||||
test('creates rule files for all modes', () => {
|
||||
// Act
|
||||
mockCreateRooStructure();
|
||||
|
||||
// Assert - check all rule files are created
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-architect', 'architect-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-ask', 'ask-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-boomerang', 'boomerang-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-code', 'code-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-debug', 'debug-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'rules-test', 'test-rules'),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
test('creates .roomodes file in project root', () => {
|
||||
// Act
|
||||
mockCreateRooStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roomodes'),
|
||||
expect.any(String)
|
||||
);
|
||||
});
|
||||
|
||||
test('creates additional required Roo directories', () => {
|
||||
// Act
|
||||
mockCreateRooStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'config'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'templates'),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.roo', 'logs'),
|
||||
{ recursive: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
113
tests/unit/profiles/rule-transformer-cline.test.js
Normal file
113
tests/unit/profiles/rule-transformer-cline.test.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
|
||||
import { clineProfile } from '../../../scripts/profiles/cline.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('Cline Rule Transformer', () => {
|
||||
const testDir = path.join(__dirname, 'temp-test-dir');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test directory before each test
|
||||
if (!fs.existsSync(testDir)) {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should correctly convert basic terms', () => {
|
||||
// Create a test Cursor rule file with basic terms
|
||||
const testCursorRule = path.join(testDir, 'basic-terms.mdc');
|
||||
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.`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testClineRule = path.join(testDir, 'basic-terms.md');
|
||||
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testClineRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('Cline');
|
||||
expect(convertedContent).toContain('cline.bot');
|
||||
expect(convertedContent).toContain('.md');
|
||||
expect(convertedContent).not.toContain('cursor.so');
|
||||
expect(convertedContent).not.toContain('Cursor rule');
|
||||
});
|
||||
|
||||
it('should correctly convert tool references', () => {
|
||||
// Create a test Cursor rule file with tool references
|
||||
const testCursorRule = path.join(testDir, 'tool-refs.mdc');
|
||||
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`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testClineRule = path.join(testDir, 'tool-refs.md');
|
||||
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testClineRule, 'utf8');
|
||||
|
||||
// Verify transformations (Cline uses standard tool names)
|
||||
expect(convertedContent).toContain('search tool');
|
||||
expect(convertedContent).toContain('edit_file tool');
|
||||
expect(convertedContent).toContain('run_command');
|
||||
expect(convertedContent).toContain('use_mcp');
|
||||
});
|
||||
|
||||
it('should correctly update file references', () => {
|
||||
// Create a test Cursor rule file with file references
|
||||
const testCursorRule = path.join(testDir, 'file-refs.mdc');
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for file references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testClineRule = path.join(testDir, 'file-refs.md');
|
||||
convertRuleToProfileRule(testCursorRule, testClineRule, clineProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testClineRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('(.clinerules/dev_workflow.md)');
|
||||
expect(convertedContent).toContain('(.clinerules/taskmaster.md)');
|
||||
expect(convertedContent).not.toContain('(mdc:.cursor/rules/');
|
||||
});
|
||||
});
|
||||
120
tests/unit/profiles/rule-transformer-cursor.test.js
Normal file
120
tests/unit/profiles/rule-transformer-cursor.test.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import {
|
||||
convertAllRulesToProfileRules,
|
||||
convertRuleToProfileRule,
|
||||
getRulesProfile
|
||||
} from '../../../src/utils/rule-transformer.js';
|
||||
import { cursorProfile } from '../../../scripts/profiles/cursor.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('Cursor Rule Transformer', () => {
|
||||
const testDir = path.join(__dirname, 'temp-test-dir');
|
||||
|
||||
beforeAll(() => {
|
||||
// Create test directory
|
||||
if (!fs.existsSync(testDir)) {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should correctly convert basic terms', () => {
|
||||
// Create a test Cursor rule file with basic terms
|
||||
const testCursorRule = path.join(testDir, 'basic-terms.mdc');
|
||||
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.`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testCursorOut = path.join(testDir, 'basic-terms.mdc');
|
||||
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
|
||||
|
||||
// Verify transformations (should preserve Cursor branding and references)
|
||||
expect(convertedContent).toContain('Cursor rule');
|
||||
expect(convertedContent).toContain('cursor.so');
|
||||
expect(convertedContent).toContain('.mdc');
|
||||
expect(convertedContent).not.toContain('roocode.com');
|
||||
expect(convertedContent).not.toContain('windsurf.com');
|
||||
});
|
||||
|
||||
it('should correctly convert tool references', () => {
|
||||
// Create a test Cursor rule file with tool references
|
||||
const testCursorRule = path.join(testDir, 'tool-refs.mdc');
|
||||
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`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testCursorOut = path.join(testDir, 'tool-refs.mdc');
|
||||
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
|
||||
|
||||
// Verify transformations (should preserve Cursor tool references)
|
||||
expect(convertedContent).toContain('search tool');
|
||||
expect(convertedContent).toContain('edit_file tool');
|
||||
expect(convertedContent).toContain('run_command');
|
||||
expect(convertedContent).toContain('use_mcp');
|
||||
expect(convertedContent).not.toContain('apply_diff');
|
||||
expect(convertedContent).not.toContain('search_files');
|
||||
});
|
||||
|
||||
it('should correctly update file references', () => {
|
||||
// Create a test Cursor rule file with file references
|
||||
const testCursorRule = path.join(testDir, 'file-refs.mdc');
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for file references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testCursorOut = path.join(testDir, 'file-refs.mdc');
|
||||
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
|
||||
|
||||
// Verify transformations (should preserve Cursor file references)
|
||||
expect(convertedContent).toContain('(mdc:.cursor/rules/dev_workflow.mdc)');
|
||||
expect(convertedContent).toContain('(mdc:.cursor/rules/taskmaster.mdc)');
|
||||
expect(convertedContent).not.toContain('(mdc:.roo/rules/');
|
||||
expect(convertedContent).not.toContain('(mdc:.windsurf/rules/');
|
||||
});
|
||||
});
|
||||
131
tests/unit/profiles/rule-transformer-roo.test.js
Normal file
131
tests/unit/profiles/rule-transformer-roo.test.js
Normal file
@@ -0,0 +1,131 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import {
|
||||
convertAllRulesToProfileRules,
|
||||
convertRuleToProfileRule,
|
||||
getRulesProfile
|
||||
} from '../../../src/utils/rule-transformer.js';
|
||||
import { rooProfile } from '../../../scripts/profiles/roo.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('Roo Rule Transformer', () => {
|
||||
const testDir = path.join(__dirname, 'temp-test-dir');
|
||||
|
||||
beforeAll(() => {
|
||||
// Create test directory
|
||||
if (!fs.existsSync(testDir)) {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should correctly convert basic terms', () => {
|
||||
// Create a test Cursor rule file with basic terms
|
||||
const testCursorRule = path.join(testDir, 'basic-terms.mdc');
|
||||
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.`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'basic-terms.md');
|
||||
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('Roo Code');
|
||||
expect(convertedContent).toContain('roocode.com');
|
||||
expect(convertedContent).toContain('.md');
|
||||
expect(convertedContent).not.toContain('cursor.so');
|
||||
expect(convertedContent).not.toContain('Cursor rule');
|
||||
});
|
||||
|
||||
it('should correctly convert tool references', () => {
|
||||
// Create a test Cursor rule file with tool references
|
||||
const testCursorRule = path.join(testDir, 'tool-refs.mdc');
|
||||
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`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'tool-refs.md');
|
||||
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('search_files tool');
|
||||
expect(convertedContent).toContain('apply_diff tool');
|
||||
expect(convertedContent).toContain('execute_command');
|
||||
expect(convertedContent).toContain('use_mcp_tool');
|
||||
});
|
||||
|
||||
it('should correctly update file references', () => {
|
||||
// Create a test Cursor rule file with file references
|
||||
const testCursorRule = path.join(testDir, 'file-refs.mdc');
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for file references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testRooRule = path.join(testDir, 'file-refs.md');
|
||||
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('(.roo/rules/dev_workflow.md)');
|
||||
expect(convertedContent).toContain('(.roo/rules/taskmaster.md)');
|
||||
expect(convertedContent).not.toContain('(mdc:.cursor/rules/');
|
||||
});
|
||||
|
||||
it('should run post-processing when converting all rules for Roo', () => {
|
||||
// Simulate a rules directory with a .mdc file
|
||||
const assetsRulesDir = path.join(testDir, 'assets', 'rules');
|
||||
fs.mkdirSync(assetsRulesDir, { recursive: true });
|
||||
const assetRule = path.join(assetsRulesDir, 'dev_workflow.mdc');
|
||||
fs.writeFileSync(assetRule, 'dummy');
|
||||
// Should create .roo/rules and call post-processing
|
||||
convertAllRulesToProfileRules(testDir, rooProfile);
|
||||
// Check for post-processing artifacts, e.g., rules-* folders or extra files
|
||||
const rooDir = path.join(testDir, '.roo');
|
||||
const found = fs.readdirSync(rooDir).some((f) => f.startsWith('rules-'));
|
||||
expect(found).toBe(true); // There should be at least one rules-* folder
|
||||
});
|
||||
});
|
||||
113
tests/unit/profiles/rule-transformer-trae.test.js
Normal file
113
tests/unit/profiles/rule-transformer-trae.test.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
|
||||
import { traeProfile } from '../../../scripts/profiles/trae.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('Trae Rule Transformer', () => {
|
||||
const testDir = path.join(__dirname, 'temp-test-dir');
|
||||
|
||||
beforeEach(() => {
|
||||
// Create test directory before each test
|
||||
if (!fs.existsSync(testDir)) {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should correctly convert basic terms', () => {
|
||||
// Create a test Cursor rule file with basic terms
|
||||
const testCursorRule = path.join(testDir, 'basic-terms.mdc');
|
||||
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.`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testTraeRule = path.join(testDir, 'basic-terms.md');
|
||||
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('Trae');
|
||||
expect(convertedContent).toContain('trae.ai');
|
||||
expect(convertedContent).toContain('.md');
|
||||
expect(convertedContent).not.toContain('cursor.so');
|
||||
expect(convertedContent).not.toContain('Cursor rule');
|
||||
});
|
||||
|
||||
it('should correctly convert tool references', () => {
|
||||
// Create a test Cursor rule file with tool references
|
||||
const testCursorRule = path.join(testDir, 'tool-refs.mdc');
|
||||
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`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testTraeRule = path.join(testDir, 'tool-refs.md');
|
||||
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');
|
||||
|
||||
// Verify transformations (Trae uses standard tool names, so no transformation)
|
||||
expect(convertedContent).toContain('search tool');
|
||||
expect(convertedContent).toContain('edit_file tool');
|
||||
expect(convertedContent).toContain('run_command');
|
||||
expect(convertedContent).toContain('use_mcp');
|
||||
});
|
||||
|
||||
it('should correctly update file references', () => {
|
||||
// Create a test Cursor rule file with file references
|
||||
const testCursorRule = path.join(testDir, 'file-refs.mdc');
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for file references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testTraeRule = path.join(testDir, 'file-refs.md');
|
||||
convertRuleToProfileRule(testCursorRule, testTraeRule, traeProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testTraeRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('(.trae/rules/dev_workflow.md)');
|
||||
expect(convertedContent).toContain('(.trae/rules/taskmaster.md)');
|
||||
expect(convertedContent).not.toContain('(mdc:.cursor/rules/');
|
||||
});
|
||||
});
|
||||
113
tests/unit/profiles/rule-transformer-windsurf.test.js
Normal file
113
tests/unit/profiles/rule-transformer-windsurf.test.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js';
|
||||
import { windsurfProfile } from '../../../scripts/profiles/windsurf.js';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
describe('Windsurf Rule Transformer', () => {
|
||||
const testDir = path.join(__dirname, 'temp-test-dir');
|
||||
|
||||
beforeAll(() => {
|
||||
// Create test directory
|
||||
if (!fs.existsSync(testDir)) {
|
||||
fs.mkdirSync(testDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
// Clean up test directory
|
||||
if (fs.existsSync(testDir)) {
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('should correctly convert basic terms', () => {
|
||||
// Create a test Cursor rule file with basic terms
|
||||
const testCursorRule = path.join(testDir, 'basic-terms.mdc');
|
||||
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.`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testWindsurfRule = path.join(testDir, 'basic-terms.md');
|
||||
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('Windsurf');
|
||||
expect(convertedContent).toContain('windsurf.com');
|
||||
expect(convertedContent).toContain('.md');
|
||||
expect(convertedContent).not.toContain('cursor.so');
|
||||
expect(convertedContent).not.toContain('Cursor rule');
|
||||
});
|
||||
|
||||
it('should correctly convert tool references', () => {
|
||||
// Create a test Cursor rule file with tool references
|
||||
const testCursorRule = path.join(testDir, 'tool-refs.mdc');
|
||||
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`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testWindsurfRule = path.join(testDir, 'tool-refs.md');
|
||||
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
|
||||
|
||||
// Verify transformations (Windsurf uses standard tool names, so no transformation)
|
||||
expect(convertedContent).toContain('search tool');
|
||||
expect(convertedContent).toContain('edit_file tool');
|
||||
expect(convertedContent).toContain('run_command');
|
||||
expect(convertedContent).toContain('use_mcp');
|
||||
});
|
||||
|
||||
it('should correctly update file references', () => {
|
||||
// Create a test Cursor rule file with file references
|
||||
const testCursorRule = path.join(testDir, 'file-refs.mdc');
|
||||
const testContent = `---
|
||||
description: Test Cursor rule for file references
|
||||
globs: **/*
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
|
||||
[taskmaster.mdc](mdc:.cursor/rules/taskmaster.mdc).`;
|
||||
|
||||
fs.writeFileSync(testCursorRule, testContent);
|
||||
|
||||
// Convert it
|
||||
const testWindsurfRule = path.join(testDir, 'file-refs.md');
|
||||
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
|
||||
|
||||
// Read the converted file
|
||||
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
|
||||
|
||||
// Verify transformations
|
||||
expect(convertedContent).toContain('(.windsurf/rules/dev_workflow.md)');
|
||||
expect(convertedContent).toContain('(.windsurf/rules/taskmaster.md)');
|
||||
expect(convertedContent).not.toContain('(mdc:.cursor/rules/');
|
||||
});
|
||||
});
|
||||
283
tests/unit/profiles/rule-transformer.test.js
Normal file
283
tests/unit/profiles/rule-transformer.test.js
Normal file
@@ -0,0 +1,283 @@
|
||||
import {
|
||||
isValidProfile,
|
||||
getRulesProfile
|
||||
} from '../../../src/utils/rule-transformer.js';
|
||||
import { RULE_PROFILES } from '../../../src/constants/profiles.js';
|
||||
|
||||
describe('Rule Transformer - General', () => {
|
||||
describe('Profile Configuration Validation', () => {
|
||||
it('should use RULE_PROFILES as the single source of truth', () => {
|
||||
// Ensure RULE_PROFILES is properly defined and contains expected profiles
|
||||
expect(Array.isArray(RULE_PROFILES)).toBe(true);
|
||||
expect(RULE_PROFILES.length).toBeGreaterThan(0);
|
||||
|
||||
// Verify expected profiles are present
|
||||
const expectedProfiles = [
|
||||
'claude',
|
||||
'cline',
|
||||
'codex',
|
||||
'cursor',
|
||||
'roo',
|
||||
'trae',
|
||||
'windsurf'
|
||||
];
|
||||
expectedProfiles.forEach((profile) => {
|
||||
expect(RULE_PROFILES).toContain(profile);
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate profiles correctly with isValidProfile', () => {
|
||||
// Test valid profiles
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
expect(isValidProfile(profile)).toBe(true);
|
||||
});
|
||||
|
||||
// Test invalid profiles
|
||||
expect(isValidProfile('invalid')).toBe(false);
|
||||
expect(isValidProfile('')).toBe(false);
|
||||
expect(isValidProfile(null)).toBe(false);
|
||||
expect(isValidProfile(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return correct rule profile with getRulesProfile', () => {
|
||||
// Test valid profiles
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
expect(profileConfig).toBeDefined();
|
||||
expect(profileConfig.profileName.toLowerCase()).toBe(profile);
|
||||
});
|
||||
|
||||
// Test invalid profile - should return null
|
||||
expect(getRulesProfile('invalid')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Profile Structure', () => {
|
||||
it('should have all required properties for each profile', () => {
|
||||
// Simple profiles that only copy files (no rule transformation)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
// Check required properties
|
||||
expect(profileConfig).toHaveProperty('profileName');
|
||||
expect(profileConfig).toHaveProperty('conversionConfig');
|
||||
expect(profileConfig).toHaveProperty('fileMap');
|
||||
expect(profileConfig).toHaveProperty('rulesDir');
|
||||
expect(profileConfig).toHaveProperty('profileDir');
|
||||
|
||||
// Simple profiles have minimal structure
|
||||
if (simpleProfiles.includes(profile)) {
|
||||
// For simple profiles, conversionConfig and fileMap can be empty
|
||||
expect(typeof profileConfig.conversionConfig).toBe('object');
|
||||
expect(typeof profileConfig.fileMap).toBe('object');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that conversionConfig has required structure for full profiles
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('profileTerms');
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('toolNames');
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('toolContexts');
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('toolGroups');
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('docUrls');
|
||||
expect(profileConfig.conversionConfig).toHaveProperty('fileReferences');
|
||||
|
||||
// Verify arrays are actually arrays
|
||||
expect(Array.isArray(profileConfig.conversionConfig.profileTerms)).toBe(
|
||||
true
|
||||
);
|
||||
expect(typeof profileConfig.conversionConfig.toolNames).toBe('object');
|
||||
expect(Array.isArray(profileConfig.conversionConfig.toolContexts)).toBe(
|
||||
true
|
||||
);
|
||||
expect(Array.isArray(profileConfig.conversionConfig.toolGroups)).toBe(
|
||||
true
|
||||
);
|
||||
expect(Array.isArray(profileConfig.conversionConfig.docUrls)).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have valid fileMap with required files for each profile', () => {
|
||||
const expectedFiles = [
|
||||
'cursor_rules.mdc',
|
||||
'dev_workflow.mdc',
|
||||
'self_improve.mdc',
|
||||
'taskmaster.mdc'
|
||||
];
|
||||
|
||||
// Simple profiles that only copy files (no rule transformation)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
// Check that fileMap exists and is an object
|
||||
expect(profileConfig.fileMap).toBeDefined();
|
||||
expect(typeof profileConfig.fileMap).toBe('object');
|
||||
expect(profileConfig.fileMap).not.toBeNull();
|
||||
|
||||
// Simple profiles can have empty fileMap since they don't transform rules
|
||||
if (simpleProfiles.includes(profile)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that fileMap is not empty for full profiles
|
||||
const fileMapKeys = Object.keys(profileConfig.fileMap);
|
||||
expect(fileMapKeys.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that all expected source files are defined in fileMap
|
||||
expectedFiles.forEach((expectedFile) => {
|
||||
expect(fileMapKeys).toContain(expectedFile);
|
||||
expect(typeof profileConfig.fileMap[expectedFile]).toBe('string');
|
||||
expect(profileConfig.fileMap[expectedFile].length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
// Verify fileMap has exactly the expected files
|
||||
expect(fileMapKeys.sort()).toEqual(expectedFiles.sort());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP Configuration Properties', () => {
|
||||
it('should have all required MCP properties for each profile', () => {
|
||||
// Simple profiles that only copy files (no MCP configuration)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
// Check MCP-related properties exist
|
||||
expect(profileConfig).toHaveProperty('mcpConfig');
|
||||
expect(profileConfig).toHaveProperty('mcpConfigName');
|
||||
expect(profileConfig).toHaveProperty('mcpConfigPath');
|
||||
|
||||
// Simple profiles have no MCP configuration
|
||||
if (simpleProfiles.includes(profile)) {
|
||||
expect(profileConfig.mcpConfig).toBe(false);
|
||||
expect(profileConfig.mcpConfigName).toBe(null);
|
||||
expect(profileConfig.mcpConfigPath).toBe(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check types for full profiles
|
||||
expect(typeof profileConfig.mcpConfig).toBe('boolean');
|
||||
expect(typeof profileConfig.mcpConfigName).toBe('string');
|
||||
expect(typeof profileConfig.mcpConfigPath).toBe('string');
|
||||
|
||||
// Check that mcpConfigPath is properly constructed
|
||||
expect(profileConfig.mcpConfigPath).toBe(
|
||||
`${profileConfig.profileDir}/${profileConfig.mcpConfigName}`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have correct MCP configuration for each profile', () => {
|
||||
const expectedConfigs = {
|
||||
claude: {
|
||||
mcpConfig: false,
|
||||
mcpConfigName: null,
|
||||
expectedPath: null
|
||||
},
|
||||
codex: {
|
||||
mcpConfig: false,
|
||||
mcpConfigName: null,
|
||||
expectedPath: null
|
||||
},
|
||||
cursor: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'mcp.json',
|
||||
expectedPath: '.cursor/mcp.json'
|
||||
},
|
||||
windsurf: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'mcp.json',
|
||||
expectedPath: '.windsurf/mcp.json'
|
||||
},
|
||||
roo: {
|
||||
mcpConfig: true,
|
||||
mcpConfigName: 'mcp.json',
|
||||
expectedPath: '.roo/mcp.json'
|
||||
},
|
||||
trae: {
|
||||
mcpConfig: false,
|
||||
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'
|
||||
}
|
||||
};
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
const expected = expectedConfigs[profile];
|
||||
|
||||
expect(profileConfig.mcpConfig).toBe(expected.mcpConfig);
|
||||
expect(profileConfig.mcpConfigName).toBe(expected.mcpConfigName);
|
||||
expect(profileConfig.mcpConfigPath).toBe(expected.expectedPath);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have consistent profileDir and mcpConfigPath relationship', () => {
|
||||
// Simple profiles that only copy files (no MCP configuration)
|
||||
const simpleProfiles = ['claude', 'codex'];
|
||||
|
||||
RULE_PROFILES.forEach((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
|
||||
// Simple profiles have null mcpConfigPath
|
||||
if (simpleProfiles.includes(profile)) {
|
||||
expect(profileConfig.mcpConfigPath).toBe(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// The mcpConfigPath should start with the profileDir
|
||||
expect(profileConfig.mcpConfigPath).toMatch(
|
||||
new RegExp(
|
||||
`^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`
|
||||
)
|
||||
);
|
||||
|
||||
// The mcpConfigPath should end with the mcpConfigName
|
||||
expect(profileConfig.mcpConfigPath).toMatch(
|
||||
new RegExp(
|
||||
`${profileConfig.mcpConfigName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have unique profile directories', () => {
|
||||
const profileDirs = RULE_PROFILES.map((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
return profileConfig.profileDir;
|
||||
});
|
||||
|
||||
// Note: Claude and Codex both use "." (root directory) so we expect some duplication
|
||||
const uniqueProfileDirs = [...new Set(profileDirs)];
|
||||
// We should have fewer unique directories than total profiles due to simple profiles using root
|
||||
expect(uniqueProfileDirs.length).toBeLessThanOrEqual(profileDirs.length);
|
||||
expect(uniqueProfileDirs.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should have unique MCP config paths', () => {
|
||||
const mcpConfigPaths = RULE_PROFILES.map((profile) => {
|
||||
const profileConfig = getRulesProfile(profile);
|
||||
return profileConfig.mcpConfigPath;
|
||||
});
|
||||
|
||||
// Note: Claude and Codex both have null mcpConfigPath so we expect some duplication
|
||||
const uniqueMcpConfigPaths = [...new Set(mcpConfigPaths)];
|
||||
// We should have fewer unique paths than total profiles due to simple profiles having null
|
||||
expect(uniqueMcpConfigPaths.length).toBeLessThanOrEqual(
|
||||
mcpConfigPaths.length
|
||||
);
|
||||
expect(uniqueMcpConfigPaths.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
556
tests/unit/profiles/selective-profile-removal.test.js
Normal file
556
tests/unit/profiles/selective-profile-removal.test.js
Normal file
@@ -0,0 +1,556 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { jest } from '@jest/globals';
|
||||
import {
|
||||
removeProfileRules,
|
||||
getRulesProfile
|
||||
} from '../../../src/utils/rule-transformer.js';
|
||||
import { removeTaskMasterMCPConfiguration } from '../../../src/utils/mcp-config-setup.js';
|
||||
|
||||
// Mock logger
|
||||
const mockLog = {
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
debug: jest.fn()
|
||||
};
|
||||
|
||||
// Mock the logger import
|
||||
jest.mock('../../../scripts/modules/utils.js', () => ({
|
||||
log: (level, message) => mockLog[level]?.(message)
|
||||
}));
|
||||
|
||||
describe('Selective Rules Removal', () => {
|
||||
let tempDir;
|
||||
let mockExistsSync;
|
||||
let mockRmSync;
|
||||
let mockReaddirSync;
|
||||
let mockReadFileSync;
|
||||
let mockWriteFileSync;
|
||||
let mockMkdirSync;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Create temp directory for testing
|
||||
tempDir = fs.mkdtempSync(path.join(process.cwd(), 'test-temp-'));
|
||||
|
||||
// Set up spies on fs methods
|
||||
mockExistsSync = jest.spyOn(fs, 'existsSync');
|
||||
mockRmSync = jest.spyOn(fs, 'rmSync').mockImplementation(() => {});
|
||||
mockReaddirSync = jest.spyOn(fs, 'readdirSync');
|
||||
mockReadFileSync = jest.spyOn(fs, 'readFileSync');
|
||||
mockWriteFileSync = jest
|
||||
.spyOn(fs, 'writeFileSync')
|
||||
.mockImplementation(() => {});
|
||||
mockMkdirSync = jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up temp directory
|
||||
try {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
|
||||
// Restore all mocked functions
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('removeProfileRules - Selective File Removal', () => {
|
||||
it('should only remove Task Master files, preserving existing rules', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const cursorProfile = getRulesProfile('cursor');
|
||||
|
||||
// Mock profile directory exists
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.cursor')) return true;
|
||||
if (filePath.includes('.cursor/rules')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
// Mock sequential calls to readdirSync to simulate the removal process
|
||||
mockReaddirSync
|
||||
// First call - get initial directory contents
|
||||
.mockReturnValueOnce([
|
||||
'cursor_rules.mdc', // Task Master file
|
||||
'dev_workflow.mdc', // Task Master file
|
||||
'self_improve.mdc', // Task Master file
|
||||
'taskmaster.mdc', // Task Master file
|
||||
'custom_rule.mdc', // Existing file (not Task Master)
|
||||
'my_company_rules.mdc' // Existing file (not Task Master)
|
||||
])
|
||||
// Second call - check remaining files after removal
|
||||
.mockReturnValueOnce([
|
||||
'custom_rule.mdc', // Remaining existing file
|
||||
'my_company_rules.mdc' // Remaining existing file
|
||||
])
|
||||
// Third call - check profile directory contents
|
||||
.mockReturnValueOnce(['rules', 'mcp.json']);
|
||||
|
||||
const result = removeProfileRules(projectRoot, cursorProfile);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.filesRemoved).toEqual([
|
||||
'cursor_rules.mdc',
|
||||
'dev_workflow.mdc',
|
||||
'self_improve.mdc',
|
||||
'taskmaster.mdc'
|
||||
]);
|
||||
expect(result.notice).toContain('Preserved 2 existing rule files');
|
||||
|
||||
// Verify only Task Master files were removed
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor/rules/cursor_rules.mdc'),
|
||||
{ force: true }
|
||||
);
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor/rules/dev_workflow.mdc'),
|
||||
{ force: true }
|
||||
);
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor/rules/self_improve.mdc'),
|
||||
{ force: true }
|
||||
);
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor/rules/taskmaster.mdc'),
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
// Verify rules directory was NOT removed (still has other files)
|
||||
expect(mockRmSync).not.toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor/rules'),
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
|
||||
// Verify profile directory was NOT removed
|
||||
expect(mockRmSync).not.toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor'),
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove empty rules directory if only Task Master files existed', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const cursorProfile = getRulesProfile('cursor');
|
||||
|
||||
// Mock profile directory exists
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.cursor')) return true;
|
||||
if (filePath.includes('.cursor/rules')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
// Mock rules directory with only Task Master files
|
||||
mockReaddirSync.mockImplementation((dirPath) => {
|
||||
if (dirPath.includes('.cursor/rules')) {
|
||||
// Before removal
|
||||
return [
|
||||
'cursor_rules.mdc',
|
||||
'dev_workflow.mdc',
|
||||
'self_improve.mdc',
|
||||
'taskmaster.mdc'
|
||||
];
|
||||
}
|
||||
if (dirPath.includes('.cursor')) {
|
||||
// After rules removal, only mcp.json remains
|
||||
return ['mcp.json'];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
// Mock empty directory after removing Task Master files
|
||||
mockReaddirSync
|
||||
.mockReturnValueOnce([
|
||||
'cursor_rules.mdc',
|
||||
'dev_workflow.mdc',
|
||||
'self_improve.mdc',
|
||||
'taskmaster.mdc'
|
||||
])
|
||||
.mockReturnValueOnce([]); // Empty after removal
|
||||
|
||||
const result = removeProfileRules(projectRoot, cursorProfile);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.filesRemoved).toEqual([
|
||||
'cursor_rules.mdc',
|
||||
'dev_workflow.mdc',
|
||||
'self_improve.mdc',
|
||||
'taskmaster.mdc'
|
||||
]);
|
||||
|
||||
// Verify rules directory was removed when empty
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor/rules'),
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove entire profile directory if completely empty and all rules were Task Master rules and MCP config deleted', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const cursorProfile = getRulesProfile('cursor');
|
||||
|
||||
// Mock profile directory exists
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.cursor')) return true;
|
||||
if (filePath.includes('.cursor/rules')) return true;
|
||||
if (filePath.includes('mcp.json')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
// Mock sequence: rules dir has only Task Master files, then empty, then profile dir empty
|
||||
mockReaddirSync
|
||||
.mockReturnValueOnce(['cursor_rules.mdc']) // Only Task Master files
|
||||
.mockReturnValueOnce([]) // rules dir empty after removal
|
||||
.mockReturnValueOnce([]); // profile dir empty after all cleanup
|
||||
|
||||
// Mock MCP config with only Task Master (will be completely deleted)
|
||||
const mockMcpConfig = {
|
||||
mcpServers: {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['task-master-ai']
|
||||
}
|
||||
}
|
||||
};
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify(mockMcpConfig));
|
||||
|
||||
const result = removeProfileRules(projectRoot, cursorProfile);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.profileDirRemoved).toBe(true);
|
||||
expect(result.mcpResult.deleted).toBe(true);
|
||||
|
||||
// Verify profile directory was removed when completely empty and conditions met
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor'),
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT remove profile directory if existing rules were preserved, even if MCP config deleted', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const cursorProfile = getRulesProfile('cursor');
|
||||
|
||||
// Mock profile directory exists
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.cursor')) return true;
|
||||
if (filePath.includes('.cursor/rules')) return true;
|
||||
if (filePath.includes('mcp.json')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
// Mock sequence: mixed rules, some remaining after removal, profile dir not empty
|
||||
mockReaddirSync
|
||||
.mockReturnValueOnce(['cursor_rules.mdc', 'my_custom_rule.mdc']) // Mixed files
|
||||
.mockReturnValueOnce(['my_custom_rule.mdc']) // Custom rule remains
|
||||
.mockReturnValueOnce(['rules', 'mcp.json']); // Profile dir has remaining content
|
||||
|
||||
// Mock MCP config with only Task Master (will be completely deleted)
|
||||
const mockMcpConfig = {
|
||||
mcpServers: {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['task-master-ai']
|
||||
}
|
||||
}
|
||||
};
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify(mockMcpConfig));
|
||||
|
||||
const result = removeProfileRules(projectRoot, cursorProfile);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.profileDirRemoved).toBe(false);
|
||||
expect(result.mcpResult.deleted).toBe(true);
|
||||
|
||||
// Verify profile directory was NOT removed (existing rules preserved)
|
||||
expect(mockRmSync).not.toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor'),
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT remove profile directory if MCP config has other servers, even if all rules were Task Master rules', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const cursorProfile = getRulesProfile('cursor');
|
||||
|
||||
// Mock profile directory exists
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.cursor')) return true;
|
||||
if (filePath.includes('.cursor/rules')) return true;
|
||||
if (filePath.includes('mcp.json')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
// Mock sequence: only Task Master rules, rules dir removed, but profile dir not empty due to MCP
|
||||
mockReaddirSync
|
||||
.mockReturnValueOnce(['cursor_rules.mdc']) // Only Task Master files
|
||||
.mockReturnValueOnce([]) // rules dir empty after removal
|
||||
.mockReturnValueOnce(['mcp.json']); // Profile dir has MCP config remaining
|
||||
|
||||
// Mock MCP config with multiple servers (Task Master will be removed, others preserved)
|
||||
const mockMcpConfig = {
|
||||
mcpServers: {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['task-master-ai']
|
||||
},
|
||||
'other-server': {
|
||||
command: 'node',
|
||||
args: ['other-server.js']
|
||||
}
|
||||
}
|
||||
};
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify(mockMcpConfig));
|
||||
|
||||
const result = removeProfileRules(projectRoot, cursorProfile);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.profileDirRemoved).toBe(false);
|
||||
expect(result.mcpResult.deleted).toBe(false);
|
||||
expect(result.mcpResult.hasOtherServers).toBe(true);
|
||||
|
||||
// Verify profile directory was NOT removed (MCP config preserved)
|
||||
expect(mockRmSync).not.toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor'),
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
});
|
||||
|
||||
it('should NOT remove profile directory if other files/folders exist, even if all other conditions are met', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const cursorProfile = getRulesProfile('cursor');
|
||||
|
||||
// Mock profile directory exists
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.cursor')) return true;
|
||||
if (filePath.includes('.cursor/rules')) return true;
|
||||
if (filePath.includes('mcp.json')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
// Mock sequence: only Task Master rules, rules dir removed, but profile dir has other files/folders
|
||||
mockReaddirSync
|
||||
.mockReturnValueOnce(['cursor_rules.mdc']) // Only Task Master files
|
||||
.mockReturnValueOnce([]) // rules dir empty after removal
|
||||
.mockReturnValueOnce(['workflows', 'custom-config.json']); // Profile dir has other files/folders
|
||||
|
||||
// Mock MCP config with only Task Master (will be completely deleted)
|
||||
const mockMcpConfig = {
|
||||
mcpServers: {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['task-master-ai']
|
||||
}
|
||||
}
|
||||
};
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify(mockMcpConfig));
|
||||
|
||||
const result = removeProfileRules(projectRoot, cursorProfile);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.profileDirRemoved).toBe(false);
|
||||
expect(result.mcpResult.deleted).toBe(true);
|
||||
expect(result.notice).toContain('Preserved 2 existing files/folders');
|
||||
|
||||
// Verify profile directory was NOT removed (other files/folders exist)
|
||||
expect(mockRmSync).not.toHaveBeenCalledWith(
|
||||
path.join(projectRoot, '.cursor'),
|
||||
{ recursive: true, force: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTaskMasterMCPConfiguration - Selective MCP Removal', () => {
|
||||
it('should only remove Task Master from MCP config, preserving other servers', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const mcpConfigPath = '.cursor/mcp.json';
|
||||
|
||||
// Mock MCP config with multiple servers
|
||||
const mockMcpConfig = {
|
||||
mcpServers: {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['task-master-ai']
|
||||
},
|
||||
'other-server': {
|
||||
command: 'node',
|
||||
args: ['other-server.js']
|
||||
},
|
||||
'another-server': {
|
||||
command: 'python',
|
||||
args: ['server.py']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify(mockMcpConfig));
|
||||
|
||||
const result = removeTaskMasterMCPConfiguration(
|
||||
projectRoot,
|
||||
mcpConfigPath
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.removed).toBe(true);
|
||||
expect(result.deleted).toBe(false);
|
||||
expect(result.hasOtherServers).toBe(true);
|
||||
|
||||
// Verify the file was written back with other servers preserved
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, mcpConfigPath),
|
||||
expect.stringContaining('other-server')
|
||||
);
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, mcpConfigPath),
|
||||
expect.stringContaining('another-server')
|
||||
);
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, mcpConfigPath),
|
||||
expect.not.stringContaining('task-master-ai')
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete entire MCP config if Task Master is the only server', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const mcpConfigPath = '.cursor/mcp.json';
|
||||
|
||||
// Mock MCP config with only Task Master
|
||||
const mockMcpConfig = {
|
||||
mcpServers: {
|
||||
'task-master-ai': {
|
||||
command: 'npx',
|
||||
args: ['task-master-ai']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify(mockMcpConfig));
|
||||
|
||||
const result = removeTaskMasterMCPConfiguration(
|
||||
projectRoot,
|
||||
mcpConfigPath
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.removed).toBe(true);
|
||||
expect(result.deleted).toBe(true);
|
||||
expect(result.hasOtherServers).toBe(false);
|
||||
|
||||
// Verify the entire file was deleted
|
||||
expect(mockRmSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, mcpConfigPath),
|
||||
{ force: true }
|
||||
);
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle MCP config with Task Master in server args', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const mcpConfigPath = '.cursor/mcp.json';
|
||||
|
||||
// Mock MCP config with Task Master referenced in args
|
||||
const mockMcpConfig = {
|
||||
mcpServers: {
|
||||
'taskmaster-wrapper': {
|
||||
command: 'npx',
|
||||
args: ['-y', '--package=task-master-ai', 'task-master-ai']
|
||||
},
|
||||
'other-server': {
|
||||
command: 'node',
|
||||
args: ['other-server.js']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockExistsSync.mockReturnValue(true);
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify(mockMcpConfig));
|
||||
|
||||
const result = removeTaskMasterMCPConfiguration(
|
||||
projectRoot,
|
||||
mcpConfigPath
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.removed).toBe(true);
|
||||
expect(result.hasOtherServers).toBe(true);
|
||||
|
||||
// Verify only the server with task-master-ai in args was removed
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, mcpConfigPath),
|
||||
expect.stringContaining('other-server')
|
||||
);
|
||||
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
||||
path.join(projectRoot, mcpConfigPath),
|
||||
expect.not.stringContaining('taskmaster-wrapper')
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-existent MCP config gracefully', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const mcpConfigPath = '.cursor/mcp.json';
|
||||
|
||||
mockExistsSync.mockReturnValue(false);
|
||||
|
||||
const result = removeTaskMasterMCPConfiguration(
|
||||
projectRoot,
|
||||
mcpConfigPath
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.removed).toBe(false);
|
||||
expect(result.deleted).toBe(false);
|
||||
expect(result.hasOtherServers).toBe(false);
|
||||
|
||||
// No file operations should have been attempted
|
||||
expect(mockReadFileSync).not.toHaveBeenCalled();
|
||||
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
||||
expect(mockRmSync).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Integration - Full Profile Removal with Preservation', () => {
|
||||
it('should handle complete removal scenario with notices', () => {
|
||||
const projectRoot = '/test/project';
|
||||
const cursorProfile = getRulesProfile('cursor');
|
||||
|
||||
// Mock mixed scenario: some Task Master files, some existing files, other MCP servers
|
||||
mockExistsSync.mockImplementation((filePath) => {
|
||||
if (filePath.includes('.cursor')) return true;
|
||||
if (filePath.includes('mcp.json')) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
// Mock sequential calls to readdirSync
|
||||
mockReaddirSync
|
||||
// First call - get initial directory contents
|
||||
.mockReturnValueOnce(['cursor_rules.mdc', 'my_custom_rule.mdc'])
|
||||
// Second call - check remaining files after removal
|
||||
.mockReturnValueOnce(['my_custom_rule.mdc'])
|
||||
// Third call - check profile directory contents
|
||||
.mockReturnValueOnce(['rules', 'mcp.json']);
|
||||
|
||||
// Mock MCP config with multiple servers
|
||||
const mockMcpConfig = {
|
||||
mcpServers: {
|
||||
'task-master-ai': { command: 'npx', args: ['task-master-ai'] },
|
||||
'other-server': { command: 'node', args: ['other.js'] }
|
||||
}
|
||||
};
|
||||
mockReadFileSync.mockReturnValue(JSON.stringify(mockMcpConfig));
|
||||
|
||||
const result = removeProfileRules(projectRoot, cursorProfile);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.filesRemoved).toEqual(['cursor_rules.mdc']);
|
||||
expect(result.notice).toContain('Preserved 1 existing rule files');
|
||||
expect(result.notice).toContain(
|
||||
'preserved other MCP server configurations'
|
||||
);
|
||||
expect(result.mcpResult.hasOtherServers).toBe(true);
|
||||
expect(result.profileDirRemoved).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
100
tests/unit/profiles/windsurf-integration.test.js
Normal file
100
tests/unit/profiles/windsurf-integration.test.js
Normal file
@@ -0,0 +1,100 @@
|
||||
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('Windsurf 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('.windsurfmodes')) {
|
||||
return 'Existing windsurfmodes content';
|
||||
}
|
||||
if (filePath.toString().includes('-rules')) {
|
||||
return 'Existing mode rules 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 Windsurf files
|
||||
function mockCreateWindsurfStructure() {
|
||||
// Create main .windsurf directory
|
||||
fs.mkdirSync(path.join(tempDir, '.windsurf'), { recursive: true });
|
||||
|
||||
// Create rules directory
|
||||
fs.mkdirSync(path.join(tempDir, '.windsurf', 'rules'), { recursive: true });
|
||||
|
||||
// Create mode-specific rule directories
|
||||
const windsurfModes = [
|
||||
'architect',
|
||||
'ask',
|
||||
'boomerang',
|
||||
'code',
|
||||
'debug',
|
||||
'test'
|
||||
];
|
||||
for (const mode of windsurfModes) {
|
||||
fs.mkdirSync(path.join(tempDir, '.windsurf', `rules-${mode}`), {
|
||||
recursive: true
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.windsurf', `rules-${mode}`, `${mode}-rules`),
|
||||
`Content for ${mode} rules`
|
||||
);
|
||||
}
|
||||
|
||||
// Copy .windsurfmodes file
|
||||
fs.writeFileSync(
|
||||
path.join(tempDir, '.windsurfmodes'),
|
||||
'Windsurfmodes file content'
|
||||
);
|
||||
}
|
||||
|
||||
test('creates all required .windsurf directories', () => {
|
||||
// Act
|
||||
mockCreateWindsurfStructure();
|
||||
|
||||
// Assert
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.windsurf'), {
|
||||
recursive: true
|
||||
});
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
path.join(tempDir, '.windsurf', 'rules'),
|
||||
{ recursive: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user