add confirmation if removing ALL rules profiles, and add --force flag on rules remove
This commit is contained in:
175
tests/unit/rules-safety-check.test.js
Normal file
175
tests/unit/rules-safety-check.test.js
Normal file
@@ -0,0 +1,175 @@
|
||||
import {
|
||||
getInstalledRulesProfiles,
|
||||
wouldRemovalLeaveNoProfiles
|
||||
} from '../../src/utils/rules-detection.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('getInstalledRulesProfiles', () => {
|
||||
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 = getInstalledRulesProfiles(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 = getInstalledRulesProfiles(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user