* move claude rules and commands to assets/claude * update claude profile to copy assets/claude to .claude * fix formatting * feat(profiles): Implement unified profile system - Convert Claude and Codex profiles to use createProfile() factory - Remove simple vs complex profile distinction in rule transformer - Unify convertAllRulesToProfileRules() to handle all profiles consistently - Fix mcpConfigPath construction in base-profile.js for null mcpConfigName - Update terminology from 'simpleProfiles' to 'assetOnlyProfiles' throughout - Ensure Claude .claude directory copying works in both CLI and MCP contexts - All profiles now follow same execution flow with proper lifecycle functions Changes: - src/profiles/claude.js: Convert to createProfile() factory pattern - src/profiles/codex.js: Convert to createProfile() factory pattern - src/utils/rule-transformer.js: Unified profile handling logic - src/utils/profiles.js: Remove simple profile categorization - src/profiles/base-profile.js: Fix mcpConfigPath construction - scripts/modules/commands.js: Update variable naming - tests/: Update all tests for unified system and terminology Fixes Claude profile asset copying issue in MCP context. All tests passing (617 passed, 11 skipped). * re-checkin claude files * fix formatting * chore: clean up test Claude rules files * chore: add changeset for unified profile system * add claude files back * add changeset * restore proper gitignore * remove claude agents file from root * remove incorrect doc * simplify profiles and update tests * update changeset * update changeset * remove profile specific code * streamline profiles with defaults and update tests * update changeset * add newline at end of gitignore * restore changes * streamline profiles with defaults; update tests and add vscode test * update rule profile tests * update wording for clearer profile management * refactor and clarify terminology * use original projectRoot var name * revert param desc * use updated claude assets from neno * add "YOUR_" before api key here * streamline codex profile * add gemini profile * update gemini profile * update tests * relocate function * update rules interactive setup Gemini desc * remove duplicative code * add comma
352 lines
12 KiB
JavaScript
352 lines
12 KiB
JavaScript
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 = {
|
|
cline: {
|
|
shouldHaveMcp: false,
|
|
expectedDir: '.clinerules',
|
|
expectedConfigName: null,
|
|
expectedPath: null
|
|
},
|
|
cursor: {
|
|
shouldHaveMcp: true,
|
|
expectedDir: '.cursor',
|
|
expectedConfigName: 'mcp.json',
|
|
expectedPath: '.cursor/mcp.json'
|
|
},
|
|
gemini: {
|
|
shouldHaveMcp: true,
|
|
expectedDir: '.gemini',
|
|
expectedConfigName: 'settings.json',
|
|
expectedPath: '.gemini/settings.json'
|
|
},
|
|
roo: {
|
|
shouldHaveMcp: true,
|
|
expectedDir: '.roo',
|
|
expectedConfigName: 'mcp.json',
|
|
expectedPath: '.roo/mcp.json'
|
|
},
|
|
trae: {
|
|
shouldHaveMcp: false,
|
|
expectedDir: '.trae',
|
|
expectedConfigName: null,
|
|
expectedPath: null
|
|
},
|
|
vscode: {
|
|
shouldHaveMcp: true,
|
|
expectedDir: '.vscode',
|
|
expectedConfigName: 'mcp.json',
|
|
expectedPath: '.vscode/mcp.json'
|
|
},
|
|
windsurf: {
|
|
shouldHaveMcp: true,
|
|
expectedDir: '.windsurf',
|
|
expectedConfigName: 'mcp.json',
|
|
expectedPath: '.windsurf/mcp.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', 'roo', 'vscode', 'windsurf'];
|
|
standardMcpProfiles.forEach((profileName) => {
|
|
const profile = getRulesProfile(profileName);
|
|
expect(profile.mcpConfigName).toBe('mcp.json');
|
|
});
|
|
});
|
|
|
|
test('should use custom settings.json for Gemini profile', () => {
|
|
const profile = getRulesProfile('gemini');
|
|
expect(profile.mcpConfigName).toBe('settings.json');
|
|
});
|
|
|
|
test('should have null config name for non-MCP profiles', () => {
|
|
const clineProfile = getRulesProfile('cline');
|
|
expect(clineProfile.mcpConfigName).toBe(null);
|
|
|
|
const traeProfile = getRulesProfile('trae');
|
|
expect(traeProfile.mcpConfigName).toBe(null);
|
|
|
|
const claudeProfile = getRulesProfile('claude');
|
|
expect(claudeProfile.mcpConfigName).toBe(null);
|
|
|
|
const codexProfile = getRulesProfile('codex');
|
|
expect(codexProfile.mcpConfigName).toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('Profile Directory Structure', () => {
|
|
test('should ensure each profile has a unique directory', () => {
|
|
const profileDirs = new Set();
|
|
// Profiles that use root directory (can share the same directory)
|
|
const rootProfiles = ['claude', 'codex', 'gemini'];
|
|
|
|
RULE_PROFILES.forEach((profileName) => {
|
|
const profile = getRulesProfile(profileName);
|
|
|
|
// Root profiles can share the root directory for rules
|
|
if (rootProfiles.includes(profileName) && profile.rulesDir === '.') {
|
|
expect(profile.rulesDir).toBe('.');
|
|
}
|
|
|
|
// Profile directories should be unique (except for root profiles)
|
|
if (!rootProfiles.includes(profileName) || profile.profileDir !== '.') {
|
|
expect(profileDirs.has(profile.profileDir)).toBe(false);
|
|
profileDirs.add(profile.profileDir);
|
|
}
|
|
});
|
|
});
|
|
|
|
test('should ensure profile directories follow expected naming convention', () => {
|
|
// Profiles that use root directory for rules
|
|
const rootRulesProfiles = ['claude', 'codex', 'gemini'];
|
|
|
|
RULE_PROFILES.forEach((profileName) => {
|
|
const profile = getRulesProfile(profileName);
|
|
|
|
// Some profiles use root directory for rules
|
|
if (
|
|
rootRulesProfiles.includes(profileName) &&
|
|
profile.rulesDir === '.'
|
|
) {
|
|
expect(profile.rulesDir).toBe('.');
|
|
}
|
|
|
|
// Profile directories (not rules directories) should follow the .name pattern
|
|
// unless they are root profiles with profileDir = '.'
|
|
if (profile.profileDir !== '.') {
|
|
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('gemini');
|
|
expect(mcpEnabledProfiles).toContain('roo');
|
|
expect(mcpEnabledProfiles).toContain('vscode');
|
|
expect(mcpEnabledProfiles).toContain('windsurf');
|
|
expect(mcpEnabledProfiles).not.toContain('claude');
|
|
expect(mcpEnabledProfiles).not.toContain('cline');
|
|
expect(mcpEnabledProfiles).not.toContain('codex');
|
|
expect(mcpEnabledProfiles).not.toContain('trae');
|
|
});
|
|
|
|
test('should provide all necessary information for MCP config creation', () => {
|
|
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);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('MCP configuration validation', () => {
|
|
const mcpProfiles = ['cursor', 'gemini', 'roo', 'windsurf', 'vscode'];
|
|
const nonMcpProfiles = ['claude', 'codex', 'cline', 'trae'];
|
|
|
|
test.each(mcpProfiles)(
|
|
'should have valid MCP config for %s profile',
|
|
(profileName) => {
|
|
const profile = getRulesProfile(profileName);
|
|
expect(profile).toBeDefined();
|
|
expect(profile.mcpConfig).toBe(true);
|
|
expect(profile.mcpConfigPath).toBeDefined();
|
|
expect(typeof profile.mcpConfigPath).toBe('string');
|
|
}
|
|
);
|
|
|
|
test.each(nonMcpProfiles)(
|
|
'should not require MCP config for %s profile',
|
|
(profileName) => {
|
|
const profile = getRulesProfile(profileName);
|
|
expect(profile).toBeDefined();
|
|
expect(profile.mcpConfig).toBe(false);
|
|
}
|
|
);
|
|
});
|
|
|
|
describe('Profile structure validation', () => {
|
|
const mcpProfiles = [
|
|
'cursor',
|
|
'gemini',
|
|
'roo',
|
|
'windsurf',
|
|
'cline',
|
|
'trae',
|
|
'vscode'
|
|
];
|
|
const profilesWithLifecycle = ['claude'];
|
|
const profilesWithoutLifecycle = ['codex'];
|
|
|
|
test.each(mcpProfiles)(
|
|
'should have file mappings for %s profile',
|
|
(profileName) => {
|
|
const profile = getRulesProfile(profileName);
|
|
expect(profile).toBeDefined();
|
|
expect(profile.fileMap).toBeDefined();
|
|
expect(typeof profile.fileMap).toBe('object');
|
|
expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
|
|
}
|
|
);
|
|
|
|
test.each(profilesWithLifecycle)(
|
|
'should have file mappings and lifecycle functions for %s profile',
|
|
(profileName) => {
|
|
const profile = getRulesProfile(profileName);
|
|
expect(profile).toBeDefined();
|
|
// Claude profile has both fileMap and lifecycle functions
|
|
expect(profile.fileMap).toBeDefined();
|
|
expect(typeof profile.fileMap).toBe('object');
|
|
expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
|
|
expect(typeof profile.onAddRulesProfile).toBe('function');
|
|
expect(typeof profile.onRemoveRulesProfile).toBe('function');
|
|
expect(typeof profile.onPostConvertRulesProfile).toBe('function');
|
|
}
|
|
);
|
|
|
|
test.each(profilesWithoutLifecycle)(
|
|
'should have file mappings without lifecycle functions for %s profile',
|
|
(profileName) => {
|
|
const profile = getRulesProfile(profileName);
|
|
expect(profile).toBeDefined();
|
|
// Codex profile has fileMap but no lifecycle functions (simplified)
|
|
expect(profile.fileMap).toBeDefined();
|
|
expect(typeof profile.fileMap).toBe('object');
|
|
expect(Object.keys(profile.fileMap).length).toBeGreaterThan(0);
|
|
expect(profile.onAddRulesProfile).toBeUndefined();
|
|
expect(profile.onRemoveRulesProfile).toBeUndefined();
|
|
expect(profile.onPostConvertRulesProfile).toBeUndefined();
|
|
}
|
|
);
|
|
});
|
|
});
|