update semantics and terminology from 'brand rules' to 'rules profiles'

This commit is contained in:
Joe Danziger
2025-05-26 19:07:10 -04:00
parent ba55615d55
commit 9db5f78da3
29 changed files with 918 additions and 513 deletions

View File

@@ -1102,31 +1102,31 @@ describe('rules command', () => {
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {});
});
test('should handle rules add <brand> command', async () => {
test('should handle rules add <profile> command', async () => {
// Simulate: task-master rules add roo
await program.parseAsync(['rules', 'add', 'roo'], { from: 'user' });
// Expect some log output indicating success
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/adding rules for brand: roo/i)
expect.stringMatching(/adding rules for profile: roo/i)
);
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/completed adding rules for brand: roo/i)
expect.stringMatching(/completed adding rules for profile: roo/i)
);
// Should not exit with error
expect(mockExit).not.toHaveBeenCalledWith(1);
});
test('should handle rules remove <brand> command', async () => {
test('should handle rules remove <profile> command', async () => {
// Simulate: task-master rules remove roo --force
await program.parseAsync(['rules', 'remove', 'roo', '--force'], {
from: 'user'
});
// Expect some log output indicating removal
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/removing rules for brand: roo/i)
expect.stringMatching(/removing rules for profile: roo/i)
);
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringMatching(/completed removal for brand: roo/i)
expect.stringMatching(/completed removal for profile: roo/i)
);
// Should not exit with error
expect(mockExit).not.toHaveBeenCalledWith(1);

View File

@@ -0,0 +1,201 @@
import { RULES_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'
},
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', () => {
RULES_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();
RULES_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', () => {
RULES_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', () => {
RULES_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 profile = getRulesProfile('cline');
expect(profile.mcpConfigName).toBe('cline_mcp_settings.json');
});
});
describe('Profile Directory Structure', () => {
test('should ensure each profile has a unique directory', () => {
const profileDirs = new Set();
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
expect(profileDirs.has(profile.profileDir)).toBe(false);
profileDirs.add(profile.profileDir);
});
});
test('should ensure profile directories follow expected naming convention', () => {
RULES_PROFILES.forEach(profileName => {
const profile = getRulesProfile(profileName);
expect(profile.profileDir).toMatch(/^\.[\w-]+$/);
});
});
});
describe('MCP Configuration Creation Logic', () => {
test('should indicate which profiles require MCP configuration creation', () => {
const mcpEnabledProfiles = RULES_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');
});
test('should provide all necessary information for MCP config creation', () => {
RULES_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
RULES_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', () => {
RULES_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
RULES_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);
}
});
});
});
});

View File

@@ -3,8 +3,9 @@ import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import {
convertRuleToBrandRule,
convertAllRulesToBrandRules
convertAllRulesToProfileRules,
convertRuleToProfileRule,
getRulesProfile
} from '../../src/utils/rule-transformer.js';
import * as cursorProfile from '../../scripts/profiles/cursor.js';
@@ -44,7 +45,7 @@ Also has references to .mdc files.`;
// Convert it
const testCursorOut = path.join(testDir, 'basic-terms.mdc');
convertRuleToBrandRule(testCursorRule, testCursorOut, cursorProfile);
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
@@ -75,7 +76,7 @@ alwaysApply: true
// Convert it
const testCursorOut = path.join(testDir, 'tool-refs.mdc');
convertRuleToBrandRule(testCursorRule, testCursorOut, cursorProfile);
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');
@@ -105,7 +106,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
// Convert it
const testCursorOut = path.join(testDir, 'file-refs.mdc');
convertRuleToBrandRule(testCursorRule, testCursorOut, cursorProfile);
convertRuleToProfileRule(testCursorRule, testCursorOut, cursorProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testCursorOut, 'utf8');

View File

@@ -3,8 +3,9 @@ import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import {
convertRuleToBrandRule,
convertAllRulesToBrandRules
convertAllRulesToProfileRules,
convertRuleToProfileRule,
getRulesProfile
} from '../../src/utils/rule-transformer.js';
import * as rooProfile from '../../scripts/profiles/roo.js';
@@ -44,7 +45,7 @@ Also has references to .mdc files.`;
// Convert it
const testRooRule = path.join(testDir, 'basic-terms.md');
convertRuleToBrandRule(testCursorRule, testRooRule, rooProfile);
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
@@ -75,7 +76,7 @@ alwaysApply: true
// Convert it
const testRooRule = path.join(testDir, 'tool-refs.md');
convertRuleToBrandRule(testCursorRule, testRooRule, rooProfile);
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
@@ -103,7 +104,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
// Convert it
const testRooRule = path.join(testDir, 'file-refs.md');
convertRuleToBrandRule(testCursorRule, testRooRule, rooProfile);
convertRuleToProfileRule(testCursorRule, testRooRule, rooProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testRooRule, 'utf8');
@@ -121,7 +122,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
const assetRule = path.join(assetsRulesDir, 'dev_workflow.mdc');
fs.writeFileSync(assetRule, 'dummy');
// Should create .roo/rules and call post-processing
convertAllRulesToBrandRules(testDir, rooProfile);
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-'));

View File

@@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { convertRuleToBrandRule } from '../../src/utils/rule-transformer.js';
import { convertRuleToProfileRule } from '../../src/utils/rule-transformer.js';
import * as windsurfProfile from '../../scripts/profiles/windsurf.js';
const __filename = fileURLToPath(import.meta.url);
@@ -41,7 +41,7 @@ Also has references to .mdc files.`;
// Convert it
const testWindsurfRule = path.join(testDir, 'basic-terms.md');
convertRuleToBrandRule(testCursorRule, testWindsurfRule, windsurfProfile);
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
@@ -72,7 +72,7 @@ alwaysApply: true
// Convert it
const testWindsurfRule = path.join(testDir, 'tool-refs.md');
convertRuleToBrandRule(testCursorRule, testWindsurfRule, windsurfProfile);
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');
@@ -100,7 +100,7 @@ This references [dev_workflow.mdc](mdc:.cursor/rules/dev_workflow.mdc) and
// Convert it
const testWindsurfRule = path.join(testDir, 'file-refs.md');
convertRuleToBrandRule(testCursorRule, testWindsurfRule, windsurfProfile);
convertRuleToProfileRule(testCursorRule, testWindsurfRule, windsurfProfile);
// Read the converted file
const convertedContent = fs.readFileSync(testWindsurfRule, 'utf8');

View File

@@ -1,67 +1,204 @@
import {
BRAND_PROFILES,
BRAND_NAMES,
isValidBrand,
getBrandProfile
isValidProfile,
getRulesProfile
} from '../../src/utils/rule-transformer.js';
import { BRAND_RULE_OPTIONS } from '../../src/constants/rules.js';
import { RULES_PROFILES } from '../../src/constants/profiles.js';
describe('Rule Transformer - General', () => {
describe('Brand Configuration Validation', () => {
it('should have BRAND_PROFILES that match BRAND_RULE_OPTIONS', () => {
// Ensure BRAND_PROFILES keys match the authoritative list from constants/rules.js
const profileKeys = Object.keys(BRAND_PROFILES).sort();
const ruleOptions = [...BRAND_RULE_OPTIONS].sort();
describe('Profile Configuration Validation', () => {
it('should use RULES_PROFILES as the single source of truth', () => {
// Ensure RULES_PROFILES is properly defined and contains expected profiles
expect(Array.isArray(RULES_PROFILES)).toBe(true);
expect(RULES_PROFILES.length).toBeGreaterThan(0);
expect(profileKeys).toEqual(ruleOptions);
// Verify expected profiles are present
const expectedProfiles = ['cline', 'cursor', 'roo', 'windsurf'];
expectedProfiles.forEach(profile => {
expect(RULES_PROFILES).toContain(profile);
});
});
it('should have BRAND_NAMES derived from BRAND_PROFILES', () => {
const expectedNames = Object.keys(BRAND_PROFILES);
expect(BRAND_NAMES).toEqual(expectedNames);
});
it('should validate brands correctly with isValidBrand', () => {
// Test valid brands
BRAND_RULE_OPTIONS.forEach(brand => {
expect(isValidBrand(brand)).toBe(true);
it('should validate profiles correctly with isValidProfile', () => {
// Test valid profiles
RULES_PROFILES.forEach((profile) => {
expect(isValidProfile(profile)).toBe(true);
});
// Test invalid brands
expect(isValidBrand('invalid')).toBe(false);
expect(isValidBrand('vscode')).toBe(false);
expect(isValidBrand('')).toBe(false);
expect(isValidBrand(null)).toBe(false);
expect(isValidBrand(undefined)).toBe(false);
// 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 brand profiles with getBrandProfile', () => {
BRAND_RULE_OPTIONS.forEach(brand => {
const profile = getBrandProfile(brand);
expect(profile).toBeDefined();
expect(profile.brandName.toLowerCase()).toBe(brand);
it('should return correct rules profile with getRulesProfile', () => {
// Test valid profiles
RULES_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
expect(profileConfig).toBeDefined();
expect(profileConfig.profileName.toLowerCase()).toBe(profile);
});
// Test invalid brand
expect(getBrandProfile('invalid')).toBeUndefined();
// Test invalid profile - should return null
expect(getRulesProfile('invalid')).toBeNull();
});
});
describe('Brand Profile Structure', () => {
it('should have all required properties for each brand profile', () => {
BRAND_RULE_OPTIONS.forEach(brand => {
const profile = BRAND_PROFILES[brand];
describe('Profile Structure', () => {
it('should have all required properties for each profile', () => {
RULES_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
// Check required properties
expect(profile).toHaveProperty('brandName');
expect(profile).toHaveProperty('conversionConfig');
expect(profile).toHaveProperty('fileMap');
expect(profile).toHaveProperty('rulesDir');
expect(profile).toHaveProperty('brandDir');
// Verify brand name matches (brandName is capitalized in profiles)
expect(profile.brandName.toLowerCase()).toBe(brand);
expect(profileConfig).toHaveProperty('profileName');
expect(profileConfig).toHaveProperty('conversionConfig');
expect(profileConfig).toHaveProperty('fileMap');
expect(profileConfig).toHaveProperty('rulesDir');
expect(profileConfig).toHaveProperty('profileDir');
// Check that conversionConfig has required structure
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'];
RULES_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();
// Check that fileMap is not empty
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', () => {
RULES_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
// Check MCP-related properties exist
expect(profileConfig).toHaveProperty('mcpConfig');
expect(profileConfig).toHaveProperty('mcpConfigName');
expect(profileConfig).toHaveProperty('mcpConfigPath');
// Check types
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 = {
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'
},
cline: {
mcpConfig: false,
mcpConfigName: 'cline_mcp_settings.json',
expectedPath: '.clinerules/cline_mcp_settings.json'
}
};
RULES_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', () => {
RULES_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
// 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 = RULES_PROFILES.map((profile) => {
const profileConfig = getRulesProfile(profile);
return profileConfig.profileDir;
});
const uniqueProfileDirs = [...new Set(profileDirs)];
expect(uniqueProfileDirs).toHaveLength(profileDirs.length);
});
it('should have unique MCP config paths', () => {
const mcpConfigPaths = RULES_PROFILES.map((profile) => {
const profileConfig = getRulesProfile(profile);
return profileConfig.mcpConfigPath;
});
const uniqueMcpConfigPaths = [...new Set(mcpConfigPaths)];
expect(uniqueMcpConfigPaths).toHaveLength(mcpConfigPaths.length);
});
});
});