UNPKG

cortexweaver

Version:

CortexWeaver is a command-line interface (CLI) tool that orchestrates a swarm of specialized AI agents, powered by Claude Code and Gemini CLI, to assist in software development. It transforms a high-level project plan (plan.md) into a series of coordinate

379 lines (375 loc) 17.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PropertyTester = void 0; const agent_1 = require("../agent"); class PropertyTester extends agent_1.Agent { getPromptTemplate() { return `Property-Based Testing Agent for invariant validation and edge case discovery. Generate comprehensive property tests with diverse input generators, mathematical/domain invariants, and shrinking strategies. Target: {{className}}, Methods: {{methods}}, Library: {{propertyLibrary}}.`; } async executeTask() { if (!this.currentTask || !this.taskContext) { throw new Error('No task or context available'); } const metadata = this.currentTask.metadata || {}; const { files, propertyLibrary = 'fast-check' } = this.taskContext; const sourceFiles = await this.readSourceFiles(files || []); const testSuite = await this.generatePropertyTestSuite(metadata?.targetClass || 'UnknownClass', sourceFiles, propertyLibrary); const testFilePath = this.generateTestFilePath(metadata?.targetClass || 'UnknownClass'); await this.writeFile(testFilePath, testSuite.code); return { testSuite: testSuite.code, invariants: testSuite.invariants, generators: testSuite.generators, coverage: testSuite.coverage, message: 'Generated property-based tests with invariant validation and comprehensive input generation' }; } async identifyInvariants(sourceCode) { const methods = this.extractMethods(sourceCode); const invariants = []; for (const method of methods) { if (this.isMathematicalOperation(method.name)) { invariants.push(...this.generateMathematicalInvariants(method)); } invariants.push(...this.generateDomainInvariants(method)); invariants.push(...this.generateGenericInvariants(method)); } return invariants; } async selectGenerators(methodSignature) { const parameters = this.extractParameters(methodSignature); return parameters.map(param => this.selectGeneratorForType(param.type, param.name)); } async generateEdgeCaseGenerators(methodSignature) { const parameters = this.extractParameters(methodSignature); const edgeCaseGenerators = []; for (const param of parameters) { edgeCaseGenerators.push(...this.getEdgeCasesForType(param.type, param.name)); } return edgeCaseGenerators; } async generateRoundTripProperties(methods) { const properties = []; const encodeMethods = methods.filter(m => m.toLowerCase().includes('encode')); const decodeMethods = methods.filter(m => m.toLowerCase().includes('decode')); for (const encode of encodeMethods) { for (const decode of decodeMethods) { if (this.areRoundTripPair(encode, decode)) { properties.push(`${decode}(${encode}(data)) === data`); } } } const serializeMethods = methods.filter(m => m.toLowerCase().includes('serialize')); const deserializeMethods = methods.filter(m => m.toLowerCase().includes('deserialize')); for (const serialize of serializeMethods) { for (const deserialize of deserializeMethods) { if (this.areRoundTripPair(serialize, deserialize)) { properties.push(`${deserialize}(${serialize}(data)) deep equals data`); } } } return properties; } async generateMetamorphicProperties(methodInfo) { const { name, parameters, returnType } = methodInfo; const properties = []; if (returnType.includes('[]') && parameters.some((p) => p.includes('[]'))) { properties.push(`${name}(array).length === array.length`); } if (name.toLowerCase().includes('sort')) { properties.push(`${name}(${name}(array)) deep equals ${name}(array)`); } if (this.isCommutativeOperation(name)) { properties.push(`${name}(a, b) === ${name}(b, a)`); } if (this.isMonotonicOperation(name)) { properties.push(`a <= b implies ${name}(a) <= ${name}(b)`); } return properties; } async generateContractProperties(methodInfo) { const { preconditions = [], postconditions = [] } = methodInfo; const properties = []; if (preconditions.length > 0) { properties.push(preconditions.join(' && ')); } for (const postcondition of postconditions) { properties.push(this.translatePostcondition(postcondition)); } return properties; } async defineShrinkingStrategy(dataType, structure) { if (typeof structure === 'object' && structure !== null) { const fields = Object.entries(structure).map(([key, type]) => { const generator = this.selectGeneratorForType(type, key); return `${key}: ${generator}`; }); return `fc.record({\n ${fields.join(',\n ')}\n})`; } return this.selectGeneratorForType(dataType, 'value'); } async validateTestQuality(testCode) { const violations = []; const suggestions = []; let score = 100; if (!testCode.includes('fc.assert') && !testCode.includes('fc.property')) { violations.push('Uses example-based testing instead of property-based'); score -= 40; } if (!testCode.includes('fc.')) { violations.push('Missing fast-check generators'); score -= 30; } const propertyCount = (testCode.match(/fc\.property/g) || []).length; if (propertyCount < 3) { violations.push('Insufficient property coverage'); score -= 20; } if (!testCode.includes('constantFrom') && !testCode.includes('oneof')) { suggestions.push('Consider adding edge case generators'); score -= 10; } if (!testCode.includes('fc.configureGlobal') && !testCode.includes('numRuns')) { suggestions.push('Consider configuring test runs and shrinking'); } if (violations.length === 0) { suggestions.push('Excellent property-based test implementation'); } else { suggestions.push('Add more property-based test patterns'); suggestions.push('Include diverse input generators'); suggestions.push('Validate mathematical/domain invariants'); } return { isValid: violations.length === 0, score: Math.max(0, score), violations, suggestions }; } async analyzeCoverage(generators, testRuns) { return { inputSpaceCoverage: this.estimateInputSpaceCoverage(generators, testRuns), edgeCasesCovered: this.countEdgeCases(generators), propertyTypes: this.identifyPropertyTypes(generators), recommendations: this.generateCoverageRecommendations(generators, testRuns) }; } async readSourceFiles(files) { const sourceFiles = []; for (const file of files) { try { const content = await this.readFile(file); sourceFiles.push(content); } catch (error) { console.warn(`Could not read file ${file}: ${error.message}`); } } return sourceFiles; } async generatePropertyTestSuite(className, sourceFiles, propertyLibrary) { const sourceCode = sourceFiles.join('\n'); const invariants = await this.identifyInvariants(sourceCode); const promptContext = { className, methods: this.extractMethods(sourceCode).map(m => m.name).join(', '), propertyLibrary, sourceFiles: sourceFiles.length.toString(), invariants: JSON.stringify(invariants, null, 2) }; const prompt = this.formatPrompt(this.getPromptTemplate(), promptContext) + ` SOURCE CODE TO TEST: ${sourceCode} IDENTIFIED INVARIANTS: ${JSON.stringify(invariants, null, 2)} Generate a comprehensive property-based test suite that: 1. Tests all identified invariants with appropriate generators 2. Includes edge case generators for boundary conditions 3. Validates mathematical and domain-specific properties 4. Implements shrinking strategies for minimal failing cases 5. Provides good input space coverage Use ${propertyLibrary} for property-based testing patterns.`; const response = await this.sendToClaude(prompt); const generators = this.extractGeneratorStrategies(response.content); const coverage = await this.analyzeCoverage(generators.map(g => g.generator), 1000); return { code: response.content, invariants, generators, coverage }; } extractMethods(sourceCode) { const methods = []; const methodMatches = sourceCode.match(/(static\s+)?(\w+)\s*\([^)]*\)\s*:\s*[^{]+/g); if (methodMatches) { methodMatches.forEach(match => { const nameMatch = match.match(/(\w+)\s*\(/); if (nameMatch) { methods.push({ name: nameMatch[1], signature: match.trim(), isStatic: match.includes('static') }); } }); } return methods; } isMathematicalOperation(methodName) { const mathOps = ['add', 'subtract', 'multiply', 'divide', 'sum', 'product', 'min', 'max']; return mathOps.some(op => methodName.toLowerCase().includes(op)); } generateMathematicalInvariants(method) { const invariants = []; const methodName = method.name.toLowerCase(); if (methodName.includes('add') || methodName.includes('sum')) { invariants.push({ name: 'commutativity', description: `${method.name}(a, b) === ${method.name}(b, a)`, applicableMethods: [method.name], generators: ['fc.integer()', 'fc.integer()'], complexity: 'simple' }, { name: 'associativity', description: `${method.name}(${method.name}(a, b), c) === ${method.name}(a, ${method.name}(b, c))`, applicableMethods: [method.name], generators: ['fc.integer()', 'fc.integer()', 'fc.integer()'], complexity: 'moderate' }, { name: 'identity', description: `${method.name}(n, 0) === n`, applicableMethods: [method.name], generators: ['fc.integer()'], complexity: 'simple' }); } if (methodName.includes('multiply')) { invariants.push({ name: 'distributivity', description: `multiply(a, add(b, c)) === add(multiply(a, b), multiply(a, c))`, applicableMethods: [method.name], generators: ['fc.integer()', 'fc.integer()', 'fc.integer()'], complexity: 'complex' }); } return invariants; } generateDomainInvariants(method) { const invariants = []; const methodName = method.name.toLowerCase(); if (methodName.includes('reverse')) { invariants.push({ name: 'reverse_involution', description: `reverse(reverse(s)) === s`, applicableMethods: [method.name], generators: ['fc.string()'], complexity: 'simple' }); } if (methodName.includes('concat')) { invariants.push({ name: 'concat_length', description: `concat(a, b).length === a.length + b.length`, applicableMethods: [method.name], generators: ['fc.string()', 'fc.string()'], complexity: 'simple' }); } return invariants; } generateGenericInvariants(method) { return [{ name: 'null_safety', description: `${method.name} handles null/undefined inputs gracefully`, applicableMethods: [method.name], generators: ['fc.constantFrom(null, undefined)'], complexity: 'simple' }]; } extractParameters(methodSignature) { const paramMatch = methodSignature.match(/\(([^)]*)\)/); if (!paramMatch) return []; const paramString = paramMatch[1]; if (!paramString.trim()) return []; return paramString.split(',').map(param => { const parts = param.trim().split(':'); return { name: parts[0].trim(), type: parts[1]?.trim() || 'any' }; }); } selectGeneratorForType(type, paramName) { const cleanType = type.replace(/\s+/g, ''); switch (cleanType) { case 'number': if (paramName.toLowerCase().includes('rate') || paramName.toLowerCase().includes('percent')) { return 'fc.float({ min: 0, max: 1 })'; } if (paramName.toLowerCase().includes('age')) { return 'fc.nat({ max: 120 })'; } if (paramName.toLowerCase().includes('income') || paramName.toLowerCase().includes('amount')) { return 'fc.float({ min: 0, max: 1000000 })'; } return 'fc.integer()'; case 'string': return 'fc.string()'; case 'boolean': return 'fc.boolean()'; case 'number[]': return 'fc.array(fc.integer())'; case 'string[]': return 'fc.array(fc.string())'; default: if (cleanType.endsWith('[]')) { return `fc.array(fc.record({ /* ${cleanType.slice(0, -2)} properties */ }))`; } return `fc.record({ /* ${cleanType} properties */ })`; } } getEdgeCasesForType(type, paramName) { const edgeCases = []; switch (type) { case 'number': edgeCases.push('fc.constantFrom(0, -0, Infinity, -Infinity, NaN)'); if (paramName.toLowerCase().includes('divisor')) { edgeCases.push('fc.constantFrom(0, -0)'); } break; case 'string': edgeCases.push('fc.constantFrom("", " ", "\\n", "\\t")'); break; case 'number[]': case 'string[]': edgeCases.push('fc.constantFrom([])'); break; } return edgeCases; } areRoundTripPair(method1, method2) { const encode = method1.toLowerCase(); const decode = method2.toLowerCase(); return (encode.includes('encode') && decode.includes('decode')) || (encode.includes('serialize') && decode.includes('deserialize')) || (encode.includes('stringify') && decode.includes('parse')); } isCommutativeOperation(name) { const commutativeOps = ['add', 'multiply', 'max', 'min', 'gcd', 'lcm']; return commutativeOps.some(op => name.toLowerCase().includes(op)); } isMonotonicOperation(name) { const monotonicOps = ['sqrt', 'log', 'abs', 'square']; return monotonicOps.some(op => name.toLowerCase().includes(op)); } translatePostcondition(postcondition) { return postcondition.replace(/balance reduced by amount/, 'newBalance === oldBalance - amount'); } estimateInputSpaceCoverage(generators, testRuns) { const complexity = generators.length * 10; return Math.min(100, (testRuns / complexity) * 100); } countEdgeCases(generators) { return generators.filter(g => g.includes('constantFrom')).length; } identifyPropertyTypes(generators) { const types = []; if (generators.some(g => g.includes('integer'))) types.push('mathematical'); if (generators.some(g => g.includes('string'))) types.push('string-based'); if (generators.some(g => g.includes('array'))) types.push('collection-based'); if (generators.some(g => g.includes('record'))) types.push('structural'); return types; } generateCoverageRecommendations(generators, testRuns) { const recommendations = []; if (testRuns < 1000) { recommendations.push('Consider increasing test runs for better coverage'); } if (!generators.some(g => g.includes('constantFrom'))) { recommendations.push('Add edge case generators with constantFrom'); } if (generators.length < 3) { recommendations.push('Consider adding more diverse generators'); } return recommendations; } extractGeneratorStrategies(testCode) { const strategies = []; const generatorMatches = testCode.match(/fc\.\w+\([^)]*\)/g); if (generatorMatches) { generatorMatches.forEach((match, index) => { strategies.push({ parameterName: `param${index}`, parameterType: 'unknown', generator: match, constraints: [], edgeCases: [] }); }); } return strategies; } generateTestFilePath(className) { const testFileName = `${className.toLowerCase()}.property.test.ts`; return `tests/property/${testFileName}`; } } exports.PropertyTester = PropertyTester; //# sourceMappingURL=property-tester.js.map