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.
206 lines (160 loc) • 7.49 kB
JavaScript
import path from 'path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ConfigService } from '../../cli/services/config-service.js';
import { StackService } from '../../cli/services/stack-service.js';
// Mock fs-extra
vi.mock('fs-extra', async () => {
const mockFunctions = {
existsSync: vi.fn(),
readFileSync: vi.fn(),
readdirSync: vi.fn(),
statSync: vi.fn(),
pathExists: vi.fn()
};
return {
...mockFunctions,
default: mockFunctions,
__esModule: true
};
});
// Import fs after mocking
import fs from 'fs-extra';
describe('Config Module', () => {
let configService;
let stackService;
const templatesDir = '/templates';
beforeEach(() => {
vi.resetAllMocks();
configService = new ConfigService({ debug: true, templatesDir });
stackService = new StackService({ debug: true, configService, templatesDir });
// Mock métodos internos
configService.debugLog = vi.fn();
});
describe('loadKitConfig', () => {
it('should load configuration from kit-config.json', () => {
const mockConfig = {
version_ranges: {
"8": "v8-9",
"9": "v8-9"
}
};
// Setup mocks
const mockJSON = JSON.stringify(mockConfig);
fs.existsSync.mockReturnValue(true);
fs.statSync.mockReturnValue({ size: mockJSON.length });
fs.readFileSync.mockReturnValue(mockJSON);
const result = configService.loadKitConfig(templatesDir);
// Just verify that it returns what's in the file
expect(fs.existsSync).toHaveBeenCalledWith(path.join(templatesDir, 'kit-config.json'));
expect(fs.readFileSync).toHaveBeenCalledWith(path.join(templatesDir, 'kit-config.json'), 'utf8');
expect(result).toEqual(mockConfig);
});
it('should return default config if config file does not exist', () => {
// Setup mock
fs.existsSync.mockReturnValue(false);
const result = configService.loadKitConfig(templatesDir);
expect(result).toBeTruthy();
expect(result.laravel).toBeDefined();
});
it('should handle JSON parse errors gracefully', () => {
// Setup mocks
fs.existsSync.mockReturnValue(true);
fs.statSync.mockReturnValue({ size: 100 });
fs.readFileSync.mockReturnValue('invalid json');
// Spy on debugLog to verify error handling
const debugSpy = vi.spyOn(configService, 'debugLog');
const result = configService.loadKitConfig(templatesDir);
expect(debugSpy).toHaveBeenCalledWith(expect.stringContaining('Error parsing kit-config.json'));
expect(result).toBeTruthy();
expect(result.laravel).toBeDefined();
});
});
describe('getAvailableArchitectures', () => {
it('should get architectures from the new location if available', () => {
const stack = 'laravel';
fs.existsSync.mockReturnValue(true);
fs.readdirSync.mockReturnValue(['standard', 'ddd', 'hexagonal']);
fs.statSync.mockReturnValue({ isDirectory: () => true });
// Usar stackService que internamente usa configService
const result = stackService.getAvailableArchitectures(stack);
expect(result.length).toBe(3);
expect(result[0].value).toBe('standard');
expect(fs.existsSync).toHaveBeenCalledWith(path.join(templatesDir, 'stacks', stack, 'architectures'));
});
it('should fallback to the old location if new location does not exist', () => {
const stack = 'laravel';
// Reseteamos el mock para tener control completo
fs.existsSync.mockReset();
// Primero se chequea el archivo de configuración
fs.existsSync.mockImplementationOnce(() => true); // kit-config.json
// Después se chequea si existe el directorio nuevo de arquitecturas
fs.existsSync.mockImplementationOnce(() => false); // stacks/laravel/architectures
// Finalmente se chequea el directorio antiguo de arquitecturas
fs.existsSync.mockImplementationOnce(() => true); // architectures/laravel
// Para cualquier otra llamada
fs.existsSync.mockImplementation(() => true);
fs.readdirSync.mockReturnValue(['standard']);
fs.statSync.mockReturnValue({ isDirectory: () => true });
const result = stackService.getAvailableArchitectures(stack);
expect(result.length).toBe(1);
expect(result[0].value).toBe('standard');
});
it('should get architectures from config if filesystem check fails', () => {
const stack = 'laravel';
const mockConfig = {
laravel: {
architectures: {
standard: { name: "Standard Laravel" },
ddd: { name: "Domain-Driven Design" }
}
}
};
fs.existsSync.mockReturnValue(false);
configService.loadKitConfig = vi.fn().mockReturnValue(mockConfig);
const result = stackService.getAvailableArchitectures(stack);
expect(result.length).toBe(2);
expect(result[0].value).toBe('standard');
expect(result[0].name).toBe('Standard Laravel');
});
});
describe('getDefaultConfig', () => {
it('should return a default configuration object', () => {
const defaultConfig = configService.getDefaultConfig();
expect(defaultConfig).toBeTruthy();
expect(defaultConfig.laravel).toBeDefined();
expect(defaultConfig.nextjs).toBeDefined();
expect(defaultConfig.react).toBeDefined();
expect(defaultConfig.global).toBeDefined();
});
});
describe('getGlobalRules', () => {
it('should return global rules from config', () => {
const mockConfig = {
global: {
always: ['README.md', 'CONTRIBUTING.md']
}
};
configService.loadKitConfig = vi.fn().mockReturnValue(mockConfig);
const globalRules = configService.getGlobalRules();
expect(globalRules).toEqual(['README.md', 'CONTRIBUTING.md']);
});
});
describe('processTemplateVariables', () => {
it('should replace template variables in content', () => {
const content = 'Project {stack} version {detectedVersion} with {versionRange}';
const meta = {
stack: 'laravel',
detectedVersion: '10',
formattedVersionName: 'v10-11'
};
const result = configService.processTemplateVariables(content, meta);
expect(result).toBe('Project laravel version 10 with v10-11');
});
it('should handle missing variables', () => {
const content = 'Project {stack} with {missingVar}';
const meta = { stack: 'laravel' };
const result = configService.processTemplateVariables(content, meta);
expect(result).toBe('Project laravel with {missingVar}');
});
});
});