// ============================================================================ // Agent 3: Test Strategy Planner // Determines what tests are needed based on code analysis // ============================================================================ import { AgentState, TestStrategy, TestPlan, TestCaseDefinition, MockDefinition, TestPriority, TestType, TestCategory, ModuleInfo, } from '../types'; import { logger } from '../utils/logger'; import * as path from 'path'; export async function strategistAgent(state: AgentState): Promise> { const startTime = Date.now(); logger.agentStart('strategist'); try { if (!state.codeAnalysis || !state.projectStructure) { throw new Error('Code analysis not available. Analyzer Agent must run first.'); } const { modules, apiEndpoints, databaseOperations, errorHandlingPatterns, patterns } = state.codeAnalysis; const unitTests: TestPlan[] = []; const integrationTests: TestPlan[] = []; const e2eTests: TestPlan[] = []; const securityTests: TestPlan[] = []; const performanceTests: TestPlan[] = []; const priorities: TestPriority[] = []; logger.agent('strategist', 'Creating test strategy...'); // 1. Generate Unit Test Plans logger.agent('strategist', 'Planning unit tests...'); for (const mod of modules) { const modPlans = createUnitTestPlans(mod); unitTests.push(...modPlans); } logger.agent('strategist', `${unitTests.length} unit test plans created`); // 2. Generate Integration Test Plans logger.agent('strategist', 'Planning integration tests...'); if (apiEndpoints.length > 0) { const apiPlans = createApiIntegrationPlans(state); integrationTests.push(...apiPlans); } if (databaseOperations.length > 0) { const dbPlans = createDatabaseIntegrationPlans(state); integrationTests.push(...dbPlans); } // Module interaction tests const interactionPlans = createModuleInteractionPlans(modules); integrationTests.push(...interactionPlans); logger.agent('strategist', `${integrationTests.length} integration test plans created`); // 3. Generate E2E Test Plans logger.agent('strategist', 'Planning E2E tests...'); if (apiEndpoints.length > 0) { const e2ePlans = createE2ETestPlans(state); e2eTests.push(...e2ePlans); } logger.agent('strategist', `${e2eTests.length} E2E test plans created`); // 4. Generate Security Test Plans logger.agent('strategist', 'Planning security tests...'); const secPlans = createSecurityTestPlans(state); securityTests.push(...secPlans); logger.agent('strategist', `${securityTests.length} security test plans created`); // 5. Generate Performance Test Plans logger.agent('strategist', 'Planning performance tests...'); const perfPlans = createPerformanceTestPlans(state); performanceTests.push(...perfPlans); logger.agent('strategist', `${performanceTests.length} performance test plans created`); // 6. Calculate priorities for (const mod of modules) { const score = calculatePriorityScore(mod, apiEndpoints.length, databaseOperations.length); priorities.push({ file: mod.filePath, score, reason: getPriorityReason(score), }); } priorities.sort((a, b) => b.score - a.score); const totalPlans = unitTests.length + integrationTests.length + e2eTests.length + securityTests.length + performanceTests.length; const totalCases = [...unitTests, ...integrationTests, ...e2eTests, ...securityTests, ...performanceTests] .reduce((sum, plan) => sum + plan.testCases.length, 0); logger.table( ['Test Type', 'Plans', 'Test Cases'], [ ['Unit Tests', String(unitTests.length), String(unitTests.reduce((s, p) => s + p.testCases.length, 0))], ['Integration Tests', String(integrationTests.length), String(integrationTests.reduce((s, p) => s + p.testCases.length, 0))], ['E2E Tests', String(e2eTests.length), String(e2eTests.reduce((s, p) => s + p.testCases.length, 0))], ['Security Tests', String(securityTests.length), String(securityTests.reduce((s, p) => s + p.testCases.length, 0))], ['Performance Tests', String(performanceTests.length), String(performanceTests.reduce((s, p) => s + p.testCases.length, 0))], ['TOTAL', String(totalPlans), String(totalCases)], ] ); const testStrategy: TestStrategy = { unitTests, integrationTests, e2eTests, securityTests, performanceTests, priority: priorities, estimatedCoverage: Math.min(95, totalCases * 2), }; logger.agentComplete('strategist', Date.now() - startTime); return { testStrategy, agentLog: [ ...state.agentLog, { agent: 'strategist', timestamp: new Date().toISOString(), action: 'Test strategy created', details: `${totalPlans} plans, ${totalCases} test cases`, duration: Date.now() - startTime, status: 'complete', }, ], }; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); logger.agentError('strategist', errMsg); return { errors: [...state.errors, `Strategist: ${errMsg}`], agentLog: [ ...state.agentLog, { agent: 'strategist', timestamp: new Date().toISOString(), action: 'Error', details: errMsg, status: 'error', }, ], }; } } function createUnitTestPlans(mod: ModuleInfo): TestPlan[] { const plans: TestPlan[] = []; // Plans for exported functions for (const func of mod.functions.filter(f => f.isExported)) { const testCases: TestCaseDefinition[] = []; // Happy path testCases.push({ name: `should execute ${func.name} successfully with valid inputs`, description: `Test normal flow of ${func.name}`, input: func.params.map(p => `valid_${p.name}`).join(', '), expectedOutput: `expected ${func.returnType}`, category: 'happy-path', edgeCases: [], }); // Edge cases for each parameter for (const param of func.params) { if (!param.optional) { testCases.push({ name: `should handle null/undefined ${param.name}`, description: `Test null/undefined for ${param.name}`, input: `${param.name}: null`, expectedOutput: 'error or fallback', category: 'null-undefined', edgeCases: ['null', 'undefined'], }); } testCases.push({ name: `should handle edge case for ${param.name}`, description: `Test boundary values for ${param.name}`, input: `${param.name}: boundary_value`, expectedOutput: 'handled correctly', category: 'boundary', edgeCases: getBoundaryValues(param.type), }); } // Error handling test if (func.isAsync) { testCases.push({ name: `should handle async errors in ${func.name}`, description: `Test async error handling`, input: 'invalid input causing async error', expectedOutput: 'rejection or error', category: 'error-handling', edgeCases: ['timeout', 'network-error', 'rejection'], }); } // Type safety testCases.push({ name: `should enforce type safety for ${func.name}`, description: `Test type safety`, input: 'wrong types', expectedOutput: 'type error or proper handling', category: 'type-safety', edgeCases: [], }); const mocks: MockDefinition[] = []; const externalDeps = mod.imports.filter(i => i.isExternal); for (const dep of externalDeps) { mocks.push({ name: dep.moduleName, module: dep.moduleName, type: 'full', }); } plans.push({ targetFile: mod.filePath, targetFunction: func.name, testType: 'unit', testCases, dependencies: mod.dependencies, mocks, priority: func.complexity > 5 ? 'critical' : func.complexity > 3 ? 'high' : 'medium', description: `Unit tests for ${func.name}`, }); } // Plans for classes for (const cls of mod.classes.filter(c => c.isExported)) { for (const method of cls.methods) { const testCases: TestCaseDefinition[] = []; testCases.push({ name: `${cls.name}.${method.name} should work correctly`, description: `Test ${cls.name}.${method.name}`, input: method.params.map(p => `valid_${p.name}`).join(', '), expectedOutput: `expected ${method.returnType}`, category: 'happy-path', edgeCases: [], }); testCases.push({ name: `${cls.name}.${method.name} should handle errors`, description: `Error handling for ${cls.name}.${method.name}`, input: 'error-inducing input', expectedOutput: 'proper error handling', category: 'error-handling', edgeCases: [], }); plans.push({ targetFile: mod.filePath, targetClass: cls.name, targetFunction: method.name, testType: 'unit', testCases, dependencies: mod.dependencies, mocks: [], priority: method.complexity > 5 ? 'critical' : 'high', description: `Unit tests for ${cls.name}.${method.name}`, }); } } return plans; } function createApiIntegrationPlans(state: AgentState): TestPlan[] { const plans: TestPlan[] = []; const endpoints = state.codeAnalysis!.apiEndpoints; for (const endpoint of endpoints) { const testCases: TestCaseDefinition[] = [ { name: `${endpoint.method} ${endpoint.path} should return success`, description: `Test successful ${endpoint.method} request`, input: `${endpoint.method} ${endpoint.path}`, expectedOutput: '200/201 response', category: 'happy-path', edgeCases: [], }, { name: `${endpoint.method} ${endpoint.path} should validate input`, description: `Test input validation`, input: 'invalid body/params', expectedOutput: '400 response', category: 'edge-case', edgeCases: ['missing-fields', 'invalid-types', 'too-long', 'sql-injection-attempt'], }, { name: `${endpoint.method} ${endpoint.path} should handle not found`, description: `Test 404 response`, input: 'non-existent resource', expectedOutput: '404 response', category: 'error-handling', edgeCases: [], }, ]; if (endpoint.authentication) { testCases.push({ name: `${endpoint.method} ${endpoint.path} should require authentication`, description: `Test auth requirement`, input: 'no auth token', expectedOutput: '401 response', category: 'security', edgeCases: ['expired-token', 'invalid-token', 'missing-token'], }); } plans.push({ targetFile: endpoint.filePath, testType: 'integration', testCases, dependencies: [], mocks: [], priority: endpoint.authentication ? 'critical' : 'high', description: `API integration tests for ${endpoint.method} ${endpoint.path}`, }); } return plans; } function createDatabaseIntegrationPlans(state: AgentState): TestPlan[] { const plans: TestPlan[] = []; const dbOps = state.codeAnalysis!.databaseOperations; // Group by file const byFile = new Map(); for (const op of dbOps) { if (!byFile.has(op.filePath)) byFile.set(op.filePath, []); byFile.get(op.filePath)!.push(op); } for (const [filePath, ops] of byFile) { const testCases: TestCaseDefinition[] = []; for (const op of ops) { testCases.push({ name: `should handle ${op.type} operation correctly`, description: `Test DB ${op.type} operation`, input: `${op.type} operation`, expectedOutput: 'successful db operation', category: 'happy-path', edgeCases: [], }); if (op.raw) { testCases.push({ name: `should prevent SQL injection in raw ${op.type}`, description: `Test SQL injection protection`, input: 'malicious SQL input', expectedOutput: 'sanitized or rejected', category: 'security', edgeCases: ['sql-injection', 'parameter-tampering'], }); } testCases.push({ name: `should handle ${op.type} failure gracefully`, description: `Test DB error handling`, input: 'failing db operation', expectedOutput: 'proper error handling', category: 'error-handling', edgeCases: ['connection-lost', 'timeout', 'constraint-violation'], }); } plans.push({ targetFile: filePath, testType: 'integration', testCases, dependencies: [], mocks: [{ name: 'database', module: 'database', type: 'full' }], priority: 'critical', description: `Database integration tests for ${path.basename(filePath)}`, }); } return plans; } function createModuleInteractionPlans(modules: ModuleInfo[]): TestPlan[] { const plans: TestPlan[] = []; for (const mod of modules) { const internalDeps = mod.imports.filter(i => !i.isExternal); if (internalDeps.length > 0) { plans.push({ targetFile: mod.filePath, testType: 'integration', testCases: [ { name: `should integrate correctly with dependencies`, description: `Test module interactions`, input: 'module interaction', expectedOutput: 'correct integration', category: 'happy-path', edgeCases: [], }, { name: `should handle dependency failures`, description: `Test dependency errors`, input: 'dependency failure', expectedOutput: 'graceful degradation', category: 'error-handling', edgeCases: ['dep-throws', 'dep-returns-null', 'dep-timeout'], }, ], dependencies: internalDeps.map(d => d.moduleName), mocks: [], priority: 'medium', description: `Module integration tests for ${path.basename(mod.filePath)}`, }); } } return plans; } function createE2ETestPlans(state: AgentState): TestPlan[] { const plans: TestPlan[] = []; const endpoints = state.codeAnalysis!.apiEndpoints; // Group endpoints by resource path const resources = new Map(); for (const ep of endpoints) { const resource = ep.path.split('/').filter(Boolean)[0] || 'root'; if (!resources.has(resource)) resources.set(resource, []); resources.get(resource)!.push(ep); } for (const [resource, eps] of resources) { const testCases: TestCaseDefinition[] = [ { name: `should complete full ${resource} workflow`, description: `Complete ${resource} E2E flow`, input: 'full workflow', expectedOutput: 'workflow completed', category: 'happy-path', edgeCases: [], }, { name: `should handle ${resource} error scenarios`, description: `E2E error scenarios for ${resource}`, input: 'error scenario', expectedOutput: 'proper error handling', category: 'error-handling', edgeCases: [], }, ]; if (eps.some(e => e.authentication)) { testCases.push({ name: `should enforce auth for ${resource} operations`, description: `Auth E2E test for ${resource}`, input: 'auth flow', expectedOutput: 'proper auth enforcement', category: 'security', edgeCases: [], }); } plans.push({ targetFile: eps[0].filePath, testType: 'e2e', testCases, dependencies: [], mocks: [], priority: 'high', description: `E2E tests for ${resource} resource`, }); } return plans; } function createSecurityTestPlans(state: AgentState): TestPlan[] { const plans: TestPlan[] = []; const endpoints = state.codeAnalysis!.apiEndpoints; const dbOps = state.codeAnalysis!.databaseOperations; // Injection tests if (endpoints.length > 0 || dbOps.length > 0) { plans.push({ targetFile: 'security', testType: 'security', testCases: [ { name: 'should prevent SQL injection', description: 'SQL Injection Test', input: "'; DROP TABLE users; --", expectedOutput: 'rejected', category: 'security', edgeCases: ['union-select', 'stacked-queries', 'blind-injection'] }, { name: 'should prevent XSS', description: 'XSS Test', input: '', expectedOutput: 'sanitized', category: 'security', edgeCases: ['stored-xss', 'reflected-xss', 'dom-xss'] }, { name: 'should prevent command injection', description: 'Command Injection Test', input: '; rm -rf /', expectedOutput: 'rejected', category: 'security', edgeCases: ['pipe', 'backtick', 'subshell'] }, { name: 'should prevent path traversal', description: 'Path Traversal Test', input: '../../../etc/passwd', expectedOutput: 'rejected', category: 'security', edgeCases: ['url-encoded', 'double-dot', 'null-byte'] }, { name: 'should prevent prototype pollution', description: 'Prototype Pollution Test', input: '{"__proto__": {"admin": true}}', expectedOutput: 'rejected', category: 'security', edgeCases: ['constructor', 'prototype-chain'] }, { name: 'should prevent SSRF', description: 'SSRF Test', input: 'http://169.254.169.254/metadata', expectedOutput: 'rejected', category: 'security', edgeCases: ['ip-bypass', 'dns-rebinding'] }, { name: 'should enforce rate limiting', description: 'Rate Limiting Test', input: 'rapid requests', expectedOutput: '429 response', category: 'security', edgeCases: [] }, { name: 'should prevent ReDoS', description: 'ReDoS Test', input: 'malicious regex input', expectedOutput: 'no hang', category: 'security', edgeCases: [] }, ], dependencies: [], mocks: [], priority: 'critical', description: 'Security Vulnerability Tests', }); } // Auth tests if (endpoints.some(e => e.authentication)) { plans.push({ targetFile: 'security-auth', testType: 'security', testCases: [ { name: 'should reject expired tokens', description: 'Token Expiry Test', input: 'expired JWT', expectedOutput: '401', category: 'security', edgeCases: [] }, { name: 'should reject tampered tokens', description: 'Token Tampering Test', input: 'modified JWT', expectedOutput: '401', category: 'security', edgeCases: ['alg-none', 'key-confusion'] }, { name: 'should prevent brute force', description: 'Brute Force Test', input: 'repeated auth attempts', expectedOutput: 'lockout', category: 'security', edgeCases: [] }, { name: 'should enforce CORS', description: 'CORS Test', input: 'cross-origin request', expectedOutput: 'proper CORS headers', category: 'security', edgeCases: [] }, ], dependencies: [], mocks: [], priority: 'critical', description: 'Authentication Security Tests', }); } return plans; } function createPerformanceTestPlans(state: AgentState): TestPlan[] { const plans: TestPlan[] = []; const endpoints = state.codeAnalysis!.apiEndpoints; if (endpoints.length > 0) { plans.push({ targetFile: 'performance', testType: 'performance', testCases: [ { name: 'should respond within acceptable time', description: 'Response time test', input: 'standard request', expectedOutput: '< 200ms', category: 'performance', edgeCases: [] }, { name: 'should handle concurrent requests', description: 'Concurrency Test', input: '100 concurrent requests', expectedOutput: 'all handled', category: 'concurrency', edgeCases: [] }, { name: 'should not have memory leaks', description: 'Memory Leak Test', input: 'repeated operations', expectedOutput: 'stable memory', category: 'performance', edgeCases: [] }, ], dependencies: [], mocks: [], priority: 'medium', description: 'Performance Tests', }); } return plans; } function getBoundaryValues(type: string): string[] { switch (type.toLowerCase()) { case 'number': return ['0', '-1', 'Number.MAX_SAFE_INTEGER', 'NaN', 'Infinity']; case 'string': return ['""', '" "', 'very-long-string', 'special-chars: <>&"\'']; case 'boolean': return ['true', 'false']; case 'array': return ['[]', '[null]', 'very-large-array']; default: return ['null', 'undefined', '{}']; } } function calculatePriorityScore(mod: ModuleInfo, apiCount: number, dbCount: number): number { let score = 0; score += mod.functions.filter(f => f.isExported).length * 10; score += mod.classes.filter(c => c.isExported).length * 15; score += mod.functions.reduce((s, f) => s + f.complexity, 0) * 2; if (mod.filePath.includes('auth') || mod.filePath.includes('security')) score += 50; if (mod.filePath.includes('controller') || mod.filePath.includes('route')) score += 30; if (mod.filePath.includes('service')) score += 20; if (mod.filePath.includes('model') || mod.filePath.includes('entity')) score += 15; return score; } function getPriorityReason(score: number): string { if (score >= 50) return 'Critical - High complexity and/or security-relevant'; if (score >= 30) return 'High - Important business logic'; if (score >= 15) return 'Medium - Standard functionality'; return 'Low - Simple utility function'; }