UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

941 lines (811 loc) 26 kB
const fs = require('fs').promises; const path = require('path'); const chalk = require('chalk'); /** * Test template system for Synkra AIOS automated test generation * Provides reusable templates for generating different types of tests */ class TestTemplateSystem { constructor(options = {}) { this.rootPath = options.rootPath || process.cwd(); this.templatesDir = path.join(this.rootPath, 'aios-core', 'templates', 'tests'); this.templateCache = new Map(); this.frameworks = this.initializeFrameworks(); this.componentTemplates = this.initializeComponentTemplates(); } /** * Initialize test template system */ async initialize() { try { // Create templates directory await fs.mkdir(this.templatesDir, { recursive: true }); // Create framework-specific template directories for (const _framework of Object.keys(this.frameworks)) { await fs.mkdir(path.join(this.templatesDir, framework), { recursive: true }); } // Load existing templates await this.loadTemplates(); console.log(chalk.green('✅ Test template system initialized')); return true; } catch (error) { console.error(chalk.red(`Failed to initialize test template system: ${error.message}`)); throw error; } } /** * Get template for specific component type and test type */ async getTemplate(componentType, testType, framework = 'jest', options = {}) { const templateKey = `${_framework}-${componentType}-${testType}`; // Check cache first if (this.templateCache.has(templateKey)) { return this.templateCache.get(templateKey); } // Load template from file or generate from base template let template = await this.loadTemplateFromFile(templateKey); if (!template) { template = await this.generateTemplate(componentType, testType, _framework, options); } // Cache the template this.templateCache.set(templateKey, template); return template; } /** * Generate test content using template */ async generateTestContent(component, testFile, config) { const template = await this.getTemplate( component.type, testFile.test_type, config.framework, { qualityLevel: config.qualityLevel, mockLevel: config.mockLevel, coverageTarget: config.coverageTarget, }, ); const templateData = { component: component, testFile: testFile, config: config, metadata: { generatedAt: new Date().toISOString(), generator: 'AIOS Test Template System', version: '1.0.0', }, }; return await this.renderTemplate(_template, templateData); } /** * Create custom template */ async createCustomTemplate(templateName, templateContent, options = {}) { const templatePath = path.join(this.templatesDir, 'custom', `${templateName}.template.js`); const templateWrapper = { name: templateName, type: options.type || 'custom', framework: options.framework || 'jest', content: templateContent, metadata: { created: new Date().toISOString(), author: options.author || 'system', description: options.description || 'Custom test template', }, }; try { await fs.mkdir(path.dirname(templatePath), { recursive: true }); await fs.writeFile(templatePath, JSON.stringify(templateWrapper, null, 2)); // Add to cache this.templateCache.set(templateName, templateWrapper); console.log(chalk.green(`✅ Custom template created: ${templateName}`)); return templateWrapper; } catch (error) { console.error(chalk.red(`Failed to create custom template: ${error.message}`)); throw error; } } /** * Load template from file */ async loadTemplateFromFile(templateKey) { const templatePath = path.join(this.templatesDir, `${templateKey}.template.js`); try { const content = await fs.readFile(templatePath, 'utf-8'); return JSON.parse(content); } catch { // Template file doesn't exist return null; } } /** * Generate template from base templates */ async generateTemplate(componentType, testType, _framework, options = {}) { const _baseTemplate = this.getBaseTemplate(componentType, testType, framework); const _frameworkConfig = this.frameworks[framework]; const template = { name: `${_framework}-${componentType}-${testType}`, type: testType, framework: framework, componentType: componentType, imports: this.generateImports(componentType, testType, _framework, options), setup: this.generateSetup(componentType, testType, _framework, options), testCases: this.generateTestCaseTemplates(componentType, testType, options), teardown: this.generateTeardown(componentType, testType, _framework, options), mocks: this.generateMockTemplates(componentType, testType, options), utilities: this.generateUtilityTemplates(componentType, testType, _framework, options), metadata: { generated: new Date().toISOString(), options: options, }, }; return template; } /** * Render template with data */ async renderTemplate(_template, data) { let content = this.frameworks[template.framework].fileTemplate; // Replace template variables content = this.replaceTemplateVariables(content, { imports: this.renderImports(template.imports, data), setup: this.renderSetup(template.setup, data), testCases: this.renderTestCases(template.testCases, data), teardown: this.renderTeardown(template.teardown, data), mocks: this.renderMocks(template.mocks, data), utilities: this.renderUtilities(template.utilities, data), metadata: this.renderMetadata(template.metadata, data), }); // Format and clean up content = this.formatGeneratedCode(content, template.framework); return content; } /** * Initialize framework configurations */ initializeFrameworks() { return { jest: { name: 'Jest', imports: { testFramework: '', assertions: '', mocks: 'jest', }, syntax: { describe: 'describe', it: 'it', test: 'test', expect: 'expect', beforeEach: 'beforeEach', afterEach: 'afterEach', beforeAll: 'beforeAll', afterAll: 'afterAll', mock: 'jest.mock', spyOn: 'jest.spyOn', }, fileTemplate: `{{imports}} {{mocks}} {{setup}} describe('{{component.name}}', () => { {{testCases}} {{teardown}} }); {{utilities}} {{metadata}}`, }, mocha: { name: 'Mocha', imports: { testFramework: "const { describe, it, beforeEach, afterEach } = require('mocha');", assertions: "const { expect } = require('chai');", mocks: "const sinon = require('sinon');", }, syntax: { describe: 'describe', it: 'it', test: 'it', expect: 'expect', beforeEach: 'beforeEach', afterEach: 'afterEach', beforeAll: 'before', afterAll: 'after', mock: 'sinon.mock', spyOn: 'sinon.spy', }, fileTemplate: `{{imports}} {{mocks}} {{setup}} describe('{{component.name}}', function() { this.timeout(5000); {{testCases}} {{teardown}} }); {{utilities}} {{metadata}}`, }, vitest: { name: 'Vitest', imports: { testFramework: "import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';", assertions: '', mocks: '', }, syntax: { describe: 'describe', it: 'it', test: 'test', expect: 'expect', beforeEach: 'beforeEach', afterEach: 'afterEach', beforeAll: 'beforeAll', afterAll: 'afterAll', mock: 'vi.mock', spyOn: 'vi.spyOn', }, fileTemplate: `{{imports}} {{mocks}} {{setup}} describe('{{component.name}}', () => { {{testCases}} {{teardown}} }); {{utilities}} {{metadata}}`, }, }; } /** * Initialize component-specific templates */ initializeComponentTemplates() { return { agent: { imports: [ "const fs = require('fs').promises;", "const path = require('path');", "const yaml = require('js-yaml');", ], setup: { variables: [ 'let agentPath;', 'let agentContent;', 'let agentConfig;', ], beforeEach: [ "agentPath = path.join(__dirname, '../../../aios-core/agents/{{component.name}}.md');", "agentContent = await fs.readFile(agentPath, 'utf-8');", 'const yamlMatch = agentContent.match(/^---\\s*\\n([\\s\\S]*?)\\n---/);', 'agentConfig = yamlMatch ? yaml.load(yamlMatch[1]) : {};', ], }, testCases: { basic: [ { name: 'should have valid YAML frontmatter', assertions: [ 'expect(agentConfig).toBeDefined();', "expect(agentConfig.name).toBe('{{component.name}}');", "expect(agentConfig.type).toBe('agent');", ], }, { name: 'should have required sections', assertions: [ "expect(agentContent).toContain('## Purpose');", "expect(agentContent).toContain('## Capabilities');", "expect(agentContent).toContain('## Usage');", ], }, ], validation: [ { name: 'should validate agent configuration', assertions: [ 'expect(agentConfig.version).toMatch(/^\\d+\\.\\d+\\.\\d+$/);', 'expect(agentConfig.capabilities).toBeInstanceOf(Array);', 'expect(agentConfig.capabilities.length).toBeGreaterThan(0);', ], }, ], }, }, task: { imports: [ "const fs = require('fs').promises;", "const path = require('path');", ], setup: { variables: [ 'let taskPath;', 'let taskContent;', 'let TaskClass;', ], beforeEach: [ "taskPath = path.join(__dirname, '../../../aios-core/tasks/{{component.name}}.md');", "taskContent = await fs.readFile(taskPath, 'utf-8');", ], }, testCases: { basic: [ { name: 'should have valid task structure', assertions: [ "expect(taskContent).toContain('# {{component.name}}');", "expect(taskContent).toContain('## Purpose');", "expect(taskContent).toContain('## Implementation');", ], }, { name: 'should have implementation code', assertions: [ "expect(taskContent).toContain('class');", "expect(taskContent).toContain('async execute(');", ], }, ], execution: [ { name: 'should execute task successfully', setup: 'const taskInstance = new TaskClass();', assertions: [ "const result = await taskInstance.execute(['test-param']);", 'expect(result).toBeDefined();', 'expect(result.success).toBe(true);', ], }, ], }, }, workflow: { imports: [ "const fs = require('fs').promises;", "const path = require('path');", "const yaml = require('js-yaml');", ], setup: { variables: [ 'let workflowPath;', 'let workflowConfig;', ], beforeEach: [ "workflowPath = path.join(__dirname, '../../../aios-core/workflows/{{component.name}}.yaml');", "const workflowContent = await fs.readFile(workflowPath, 'utf-8');", 'workflowConfig = yaml.load(workflowContent);', ], }, testCases: { basic: [ { name: 'should have valid workflow structure', assertions: [ 'expect(workflowConfig).toBeDefined();', "expect(workflowConfig.name).toBe('{{component.name}}');", 'expect(workflowConfig.steps).toBeInstanceOf(Array);', ], }, { name: 'should have required metadata', assertions: [ 'expect(workflowConfig.version).toBeDefined();', 'expect(workflowConfig.description).toBeDefined();', 'expect(workflowConfig.steps.length).toBeGreaterThan(0);', ], }, ], validation: [ { name: 'should validate workflow steps', assertions: [ 'workflowConfig.steps.forEach(step => {', ' expect(step.name).toBeDefined();', ' expect(step.action).toBeDefined();', '});', ], }, ], }, }, util: { imports: [ "const path = require('path');", ], setup: { variables: [ 'let UtilClass;', 'let utilInstance;', ], beforeEach: [ "UtilClass = require('../../../aios-core/scripts/{{component.name}}');", 'utilInstance = new UtilClass();', ], }, testCases: { basic: [ { name: 'should instantiate correctly', assertions: [ 'expect(utilInstance).toBeDefined();', "expect(utilInstance.constructor.name).toBe('{{componentClass}}');", ], }, { name: 'should have required methods', assertions: [ "expect(typeof utilInstance.initialize).toBe('function');", ], }, ], functionality: [ { name: 'should initialize successfully', assertions: [ 'const result = await utilInstance.initialize();', 'expect(result).toBeTruthy();', ], }, ], }, }, }; } /** * Generate imports section */ generateImports(componentType, testType, _framework, options) { const _frameworkConfig = this.frameworks[framework]; const componentTemplate = this.componentTemplates[componentType]; const imports = [ frameworkConfig.imports.testFramework, frameworkConfig.imports.assertions, frameworkConfig.imports.mocks, ].filter(imp => imp); // Add component-specific imports if (componentTemplate && componentTemplate.imports) { imports.push(...componentTemplate.imports); } // Add integration/e2e specific imports if (testType === 'integration') { imports.push( "const request = require('supertest');", "const { setupTestDB, teardownTestDB } = require('../../helpers/db-helper');", ); } if (testType === 'e2e') { imports.push( "const { setupTestEnvironment, cleanupTestEnvironment } = require('../../helpers/e2e-helper');", ); } return imports; } /** * Generate setup section */ generateSetup(componentType, testType, _framework, options) { const componentTemplate = this.componentTemplates[componentType]; const setup = { variables: [], beforeAll: [], beforeEach: [], afterEach: [], afterAll: [], }; // Add component-specific setup if (componentTemplate && componentTemplate.setup) { setup.variables.push(...(componentTemplate.setup.variables || [])); setup.beforeEach.push(...(componentTemplate.setup.beforeEach || [])); } // Add test type specific setup if (testType === 'integration') { setup.beforeAll.push('await setupTestDB();'); setup.afterAll.push('await teardownTestDB();'); } if (testType === 'e2e') { setup.beforeAll.push('await setupTestEnvironment();'); setup.afterAll.push('await cleanupTestEnvironment();'); } return setup; } /** * Generate test case templates */ generateTestCaseTemplates(componentType, testType, options) { const componentTemplate = this.componentTemplates[componentType]; const testCases = []; if (componentTemplate && componentTemplate.testCases) { // Add basic test cases if (componentTemplate.testCases.basic) { testCases.push(...componentTemplate.testCases.basic); } // Add specific test cases based on quality level if (options.qualityLevel === 'comprehensive') { if (componentTemplate.testCases.validation) { testCases.push(...componentTemplate.testCases.validation); } if (componentTemplate.testCases.execution) { testCases.push(...componentTemplate.testCases.execution); } if (componentTemplate.testCases.functionality) { testCases.push(...componentTemplate.testCases.functionality); } } } // Add generic test cases if no specific ones found if (testCases.length === 0) { testCases.push({ name: 'should be defined', assertions: ['expect({{component.name}}).toBeDefined();'], }); } return testCases; } /** * Generate teardown section */ generateTeardown(componentType, testType, _framework, options) { const teardown = { afterEach: [], afterAll: [], }; // Add component-specific teardown if (componentType === 'util') { teardown.afterEach.push('if (utilInstance && utilInstance.cleanup) await utilInstance.cleanup();'); } return teardown; } /** * Generate mock templates */ generateMockTemplates(componentType, testType, options) { const mocks = []; if (options.mockLevel === 'extensive') { // Add comprehensive mocking mocks.push({ type: 'module', target: 'fs', implementation: '{ promises: { readFile: jest.fn(), writeFile: jest.fn() } }', }); } if (options.mockLevel !== 'minimal') { // Add moderate mocking if (componentType === 'agent' || componentType === 'workflow') { mocks.push({ type: 'module', target: 'js-yaml', implementation: '{ load: jest.fn(), dump: jest.fn() }', }); } } return mocks; } /** * Generate utility templates */ generateUtilityTemplates(componentType, testType, _framework, options) { const utilities = []; // Add helper functions for specific component types if (componentType === 'agent') { utilities.push({ name: 'createMockAgentConfig', implementation: `function createMockAgentConfig(overrides = {}) { return { name: '{{component.name}}', type: 'agent', version: '1.0.0', capabilities: ['test'], ...overrides }; }`, }); } if (testType === 'integration') { utilities.push({ name: 'createTestContext', implementation: `function createTestContext() { return { testId: 'test-' + Date.now(), cleanup: [] }; }`, }); } return utilities; } /** * Render template sections */ renderImports(imports, data) { return imports.filter(imp => imp).join('\n'); } renderSetup(setup, data) { let content = ''; if (setup.variables.length > 0) { content += setup.variables.join('\n') + '\n\n'; } if (setup.beforeAll.length > 0) { content += `beforeAll(async () => { ${setup.beforeAll.join('\n ')} }); `; } if (setup.beforeEach.length > 0) { content += `beforeEach(async () => { ${setup.beforeEach.join('\n ')} }); `; } return content; } renderTestCases(testCases, data) { return testCases.map(testCase => { let caseContent = ` it('${testCase.name}', async () => {\n`; if (testCase.setup) { caseContent += ` ${testCase.setup}\n\n`; } if (testCase.assertions) { caseContent += testCase.assertions.map(assertion => ` ${assertion}`).join('\n'); } caseContent += '\n });'; return caseContent; }).join('\n\n'); } renderTeardown(teardown, data) { let content = ''; if (teardown.afterEach.length > 0) { content += ` afterEach(async () => { ${teardown.afterEach.join('\n ')} });`; } if (teardown.afterAll.length > 0) { content += ` afterAll(async () => { ${teardown.afterAll.join('\n ')} });`; } return content; } renderMocks(mocks, data) { if (mocks.length === 0) return ''; return mocks.map(mock => { if (mock.type === 'module') { return `jest.mock('${mock.target}', () => (${mock.implementation}));`; } return ''; }).filter(mock => mock).join('\n') + '\n'; } renderUtilities(utilities, data) { if (utilities.length === 0) return ''; return '\n// Test Utilities\n' + utilities.map(util => util.implementation).join('\n\n'); } renderMetadata(metadata, data) { return ` // Generated by AIOS Test Template System // Generated at: ${data.metadata.generatedAt} // Component: ${data.component.type}/${data.component.name} // Framework: ${data.config.framework}`; } /** * Replace template variables in content */ replaceTemplateVariables(content, sections) { let result = content; Object.entries(sections).forEach(([key, value]) => { const placeholder = `{{${key}}}`; result = result.replace(new RegExp(placeholder, 'g'), value || ''); }); // Replace component-specific variables result = result.replace(/\{\{component\.name\}\}/g, '${data.component.name}'); result = result.replace(/\{\{component\.type\}\}/g, '${data.component.type}'); result = result.replace(/\{\{componentClass\}\}/g, '${this.toClassName(data.component.name)}'); return result; } /** * Format generated code */ formatGeneratedCode(content, framework) { // Remove excessive blank lines content = content.replace(/\n{3,}/g, '\n\n'); // Ensure proper spacing around blocks content = content.replace(/}\n{/g, '}\n\n{'); // Clean up any template artifacts content = content.replace(/\{\{[^}]+\}\}/g, ''); return content.trim(); } /** * Load all templates from disk */ async loadTemplates() { try { const templateFiles = await this.findTemplateFiles(); for (const templateFile of templateFiles) { try { const content = await fs.readFile(templateFile, 'utf-8'); const template = JSON.parse(content); const templateKey = path.basename(templateFile, '.template.js'); this.templateCache.set(templateKey, template); } catch (error) { console.warn(chalk.yellow(`Failed to load template ${templateFile}: ${error.message}`)); } } console.log(chalk.gray(`Loaded ${this.templateCache.size} template(s)`)); } catch (error) { console.warn(chalk.yellow(`Failed to load templates: ${error.message}`)); } } /** * Find all template files */ async findTemplateFiles() { const templateFiles = []; try { const entries = await fs.readdir(this.templatesDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const subDir = path.join(this.templatesDir, entry.name); const subFiles = await fs.readdir(subDir); for (const subFile of subFiles) { if (subFile.endsWith('.template.js')) { templateFiles.push(path.join(subDir, subFile)); } } } else if (entry.name.endsWith('.template.js')) { templateFiles.push(path.join(this.templatesDir, entry.name)); } } } catch { // Templates directory doesn't exist yet } return templateFiles; } /** * Get base template for component and test type */ getBaseTemplate(componentType, testType, framework) { return { componentType, testType, _framework, baseStructure: this.frameworks[framework].fileTemplate, }; } /** * Convert component name to class name */ toClassName(name) { return name.split('-') .map(part => part.charAt(0).toUpperCase() + part.slice(1)) .join(''); } /** * Get available templates */ getAvailableTemplates() { const templates = {}; for (const [key, template] of this.templateCache.entries()) { const [framework, componentType, testType] = key.split('-'); if (!templates[framework]) templates[framework] = {}; if (!templates[framework][componentType]) templates[framework][componentType] = []; templates[framework][componentType].push(_testType); } return templates; } /** * Validate template */ validateTemplate(_template) { const required = ['name', 'type', 'framework', 'componentType']; for (const field of required) { if (!template[field]) { throw new Error(`Template missing required field: ${field}`); } } if (!this.frameworks[template.framework]) { throw new Error(`Unsupported framework: ${template.framework}`); } return true; } } module.exports = TestTemplateSystem;