UNPKG

agent-rules-kit

Version:

CLI tool to bootstrap AI agent rules for multiple IDEs and frameworks. Generates optimized rules for Cursor, VS Code, Claude, and 6+ other AI coding assistants.

391 lines (309 loc) 14.3 kB
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { FileService } from '../../../cli/services/file-service.js'; describe('FileService', () => { let fileService; beforeEach(() => { vi.clearAllMocks(); // Crear un objeto mock para las funciones del FileService fileService = new FileService({ debug: true }); // Mock de los métodos heredados de BaseService fileService.debugLog = vi.fn(); fileService.readFile = vi.fn().mockReturnValue('# Test content'); fileService.writeFile = vi.fn(); fileService.directoryExists = vi.fn().mockReturnValue(true); fileService.ensureDirectoryExists = vi.fn(); fileService.getFilesInDirectory = vi.fn().mockReturnValue(['test.md']); }); describe('addFrontMatter', () => { it('should format metadata correctly', () => { const body = '# Test markdown'; const meta = { title: 'Test', description: 'Test description' }; const result = fileService.addFrontMatter(body, meta); expect(result).toBe('---\ntitle: Test\ndescription: Test description\n---\n# Test markdown'); }); }); describe('processTemplateVariables', () => { it('should replace template variables with their values', () => { const content = 'Stack: {stack}, Version: {detectedVersion}, Range: {versionRange}, Path: {projectPath}'; const meta = { stack: 'test-stack', detectedVersion: '1.0', versionRange: 'v1', projectPath: './test', debug: true }; const result = fileService.processTemplateVariables(content, meta); expect(result).toBe('Stack: test-stack, Version: 1.0, Range: v1, Path: ./test'); }); it('should normalize projectPath when it is empty or "."', () => { const content = 'Path: {projectPath}'; const result1 = fileService.processTemplateVariables(content, { projectPath: '' }); const result2 = fileService.processTemplateVariables(content, { projectPath: '.' }); expect(result1).toBe('Path: ./'); expect(result2).toBe('Path: ./'); }); it('should not replace variables that are not provided', () => { const content = 'Stack: {stack}, Missing: {missing}'; const meta = { stack: 'test-stack' }; const result = fileService.processTemplateVariables(content, meta); expect(result).toBe('Stack: test-stack, Missing: {missing}'); }); }); describe('wrapMdToMdc', () => { it('should process global rules correctly', () => { const src = '/path/to/global/test.md'; const destFile = '/output/test.mdc'; const meta = { debug: true }; const config = { global: { always: ['test.md'] } }; fileService.wrapMdToMdc(src, destFile, meta, config); // Verificar que se llamó a writeFile con el frontmatter correcto expect(fileService.writeFile).toHaveBeenCalled(); expect(fileService.readFile).toHaveBeenCalledWith(src); }); it('should process stack-specific rules correctly', () => { const src = '/path/to/stacks/laravel/test.md'; const destFile = '/output/test.mdc'; const meta = { stack: 'laravel', projectPath: './test', debug: true }; const config = { laravel: { globs: ['<root>/app/**/*.php'] } }; fileService.wrapMdToMdc(src, destFile, meta, config); // Verificar que se llamó a writeFile con el frontmatter correcto expect(fileService.writeFile).toHaveBeenCalled(); expect(fileService.readFile).toHaveBeenCalledWith(src); }); }); describe('copyRuleGroup', () => { it('should not process files if directory does not exist', () => { fileService.directoryExists.mockReturnValue(false); fileService.copyRuleGroup('/nonexistent/path', '/dest', { debug: true }); expect(fileService.getFilesInDirectory).not.toHaveBeenCalled(); }); it('should process documentation files differently than rule files', () => { const tmplDir = '/templates/rules'; const destDir = '/dest/docs'; const meta = { stack: 'test', debug: true }; fileService.copyRuleGroup(tmplDir, destDir, meta); expect(fileService.readFile).toHaveBeenCalled(); expect(fileService.writeFile).toHaveBeenCalled(); expect(fileService.debugLog).toHaveBeenCalled(); }); it('should convert .md to .mdc for non-docs directories', () => { const tmplDir = '/templates/rules'; const destDir = '/dest/rules'; const meta = { stack: 'test', debug: true }; fileService.copyRuleGroup(tmplDir, destDir, meta); expect(fileService.debugLog).toHaveBeenCalled(); }); }); describe('unwrapMdcToMd', () => { it('should remove frontmatter from MDC content', () => { const mdcContent = `--- globs: "**/*" alwaysApply: true --- # Test Heading This is test content.`; const result = fileService.unwrapMdcToMd(mdcContent); expect(result).not.toContain('---'); expect(result).not.toContain('globs:'); expect(result).not.toContain('alwaysApply:'); expect(result).toBe('# Test Heading\n\nThis is test content.'); }); it('should return original content if no frontmatter', () => { const content = '# No Frontmatter\n\nJust regular content.'; const result = fileService.unwrapMdcToMd(content); expect(result).toBe(content); }); it('should handle malformed frontmatter gracefully', () => { const badContent = `--- This is not proper frontmatter # Heading`; const result = fileService.unwrapMdcToMd(badContent); // Should return original content if frontmatter is malformed expect(result).toBe(badContent); }); }); describe('exportMdcToMd', () => { it('should export MDC file to MD by removing frontmatter', async () => { const mdcContent = `--- globs: "**/*" --- # Content`; // Mock file reading and writing fileService.readFileOptimized = vi.fn().mockResolvedValue(mdcContent); fileService.writeFileAsync = vi.fn().mockResolvedValue(); await fileService.exportMdcToMd('source.mdc', 'dest.md'); expect(fileService.readFileOptimized).toHaveBeenCalledWith('source.mdc'); expect(fileService.writeFileAsync).toHaveBeenCalledWith('dest.md', '# Content'); }); it('should handle errors when exporting', async () => { fileService.readFileOptimized = vi.fn().mockRejectedValue(new Error('Read error')); await expect(fileService.exportMdcToMd('source.mdc', 'dest.md')).rejects.toThrow('Read error'); }); }); describe('extractFrontmatter', () => { it('should extract frontmatter and content correctly', () => { const mdContent = `--- globs: '<root>/app/**/*.php' alwaysApply: false description: 'Testing best practices' --- # Test Content This is test content.`; const result = fileService.extractFrontmatter(mdContent); expect(result).toHaveProperty('frontmatter'); expect(result).toHaveProperty('content'); expect(result.frontmatter.globs).toBe('<root>/app/**/*.php'); expect(result.frontmatter.alwaysApply).toBe(false); expect(result.frontmatter.description).toBe('Testing best practices'); expect(result.content).toBe('# Test Content\n\nThis is test content.'); }); it('should handle content without frontmatter', () => { const mdContent = '# Test Content\n\nThis is test content without frontmatter.'; const result = fileService.extractFrontmatter(mdContent); expect(result.frontmatter).toEqual({}); expect(result.content).toBe(mdContent); }); it('should handle arrays in frontmatter', () => { const mdContent = `--- globs: ['/app/**/*.php', '/tests/**/*.php'] alwaysApply: true --- # Content`; const result = fileService.extractFrontmatter(mdContent); expect(Array.isArray(result.frontmatter.globs)).toBe(true); expect(result.frontmatter.globs).toContain('/app/**/*.php'); expect(result.frontmatter.globs).toContain('/tests/**/*.php'); }); it('should handle boolean values in frontmatter', () => { const mdContent = `--- alwaysApply: true isActive: false --- # Content`; const result = fileService.extractFrontmatter(mdContent); expect(result.frontmatter.alwaysApply).toBe(true); expect(result.frontmatter.isActive).toBe(false); }); }); describe('combineMdFiles', () => { beforeEach(() => { // Mock file reading and template processing fileService.processTemplateVariables = vi.fn(content => content); // Usar mock functions para evitar redefinir propiedades fileService.pathExists = vi.fn().mockResolvedValue(true); fileService.readFileOptimized = vi.fn().mockImplementation(async (filePath) => { if (filePath.includes('base')) { return `--- globs: '<root>/app/**/*.php' alwaysApply: false description: 'Base testing practices' --- # Base Content`; } else if (filePath.includes('version')) { return `--- globs: '<root>/tests/**/*.php' alwaysApply: false description: 'Version-specific testing practices' --- # Version Content`; } return ''; }); }); it('should combine multiple files correctly', async () => { // Modificar el orden para que la ruta con 'v12' aparezca al final const filePaths = [ '/templates/stacks/laravel/base/testing.md', '/templates/stacks/laravel/v12/testing.md' ]; // Asegurarnos de que el archivo de versión sea reconocido como de versión fileService.readFileOptimized = vi.fn().mockImplementation(async (filePath) => { if (filePath.includes('base')) { return `--- globs: '<root>/app/**/*.php' alwaysApply: false description: 'Base testing practices' --- # Base Content`; } else if (filePath.includes('v12')) { return `--- globs: '<root>/tests/**/*.php' alwaysApply: false description: 'Version-specific testing practices' --- # Version Content`; } return ''; }); const meta = { stack: 'laravel', versionRange: 'v12' }; const result = await fileService.combineMdFiles(filePaths, meta); expect(result).toHaveProperty('frontmatter'); expect(result).toHaveProperty('content'); // Version-specific frontmatter should override base frontmatter expect(result.frontmatter.globs).toBe('<root>/tests/**/*.php'); expect(result.frontmatter.description).toBe('Version-specific testing practices'); // Content should include both files with proper headers expect(result.content).toContain('# Base Content'); expect(result.content).toContain('Version specific'); expect(result.content).toContain('# Version Content'); }); it('should handle single file correctly', async () => { const filePaths = ['/templates/stacks/laravel/base/testing.md']; const meta = { stack: 'laravel' }; const result = await fileService.combineMdFiles(filePaths, meta); expect(result.content).toBe('# Base Content'); expect(result.frontmatter.description).toBe('Base testing practices'); }); it('should process template variables in each file', async () => { // Restore the original method to verify it's called fileService.processTemplateVariables = vi.fn(content => content.replace('{stack}', 'laravel')); const filePaths = [ '/templates/stacks/laravel/base/testing.md', '/templates/stacks/laravel/v12/testing.md' ]; fileService.readFileOptimized = vi.fn().mockImplementation(async (filePath) => { if (filePath.includes('base')) { return `--- globs: '<root>/app/**/*.php' --- # {stack} Base Content`; } else { return `--- globs: '<root>/tests/**/*.php' --- # {stack} Version Content`; } }); const meta = { stack: 'laravel', versionRange: 'v12' }; const result = await fileService.combineMdFiles(filePaths, meta); expect(fileService.processTemplateVariables).toHaveBeenCalled(); expect(result.content).toContain('# laravel Base Content'); expect(result.content).toContain('# laravel Version Content'); }); it('should handle non-existent files', async () => { fileService.pathExists = vi.fn().mockImplementation(async (filePath) => { return filePath.includes('base'); // Only base file exists }); const filePaths = [ '/templates/stacks/laravel/base/testing.md', '/templates/stacks/laravel/v12/testing.md' ]; const meta = { stack: 'laravel' }; const result = await fileService.combineMdFiles(filePaths, meta); expect(result.content).toBe('# Base Content'); expect(fileService.readFileOptimized).toHaveBeenCalledTimes(1); // Only called for the existing file }); }); });