// ============================================================================ // LangGraph Workflow Orchestration // Defines the multi-agent pipeline with state management // ============================================================================ import chalk from 'chalk'; import boxen from 'boxen'; import figures from 'figures'; import { AgentState, AgentName, CliMode } from '../types'; import { scannerNode, analyzerNode, strategistNode, writerNode, reviewerNode, runnerNode, securityNode, reporterNode, shouldRewrite, NodeFunction, } from './nodes'; import { createInitialState, mergeState } from './state'; import { SuiteConfig } from '../types'; import { logger } from '../utils/logger'; /** * Workflow definition: ordered agent pipeline with conditional edges. * * Full Pipeline: * Scanner -> Analyzer -> Strategist -> Writer -> Reviewer -+-> Runner -> Security -> Reporter * | * +-> Writer (if review fails, max 1 retry) */ interface WorkflowStep { name: AgentName; node: NodeFunction; description: string; } const FULL_PIPELINE: WorkflowStep[] = [ { name: 'scanner', node: scannerNode, description: 'Scan project structure' }, { name: 'analyzer', node: analyzerNode, description: 'Deep code analysis' }, { name: 'strategist', node: strategistNode, description: 'Create test strategy' }, { name: 'writer', node: writerNode, description: 'Write tests' }, { name: 'reviewer', node: reviewerNode, description: 'Review tests' }, { name: 'runner', node: runnerNode, description: 'Execute tests' }, { name: 'security', node: securityNode, description: 'Security audit' }, { name: 'reporter', node: reporterNode, description: 'Generate reports' }, ]; const AGENT_COLORS: Record string> = { scanner: chalk.cyan, analyzer: chalk.blue, strategist: chalk.magenta, writer: chalk.green, reviewer: chalk.yellow, runner: chalk.redBright, security: chalk.red, reporter: chalk.white, }; /** * Get pipeline steps based on CLI mode. */ function getPipeline(mode: CliMode): WorkflowStep[] { switch (mode) { case 'analyze': return FULL_PIPELINE.filter(s => ['scanner', 'analyzer'].includes(s.name)); case 'generate': return FULL_PIPELINE.filter(s => ['scanner', 'analyzer', 'strategist', 'writer', 'reviewer'].includes(s.name)); case 'run': return FULL_PIPELINE.filter(s => ['scanner', 'analyzer', 'strategist', 'writer', 'runner'].includes(s.name)); case 'security': return FULL_PIPELINE.filter(s => ['scanner', 'analyzer', 'security', 'reporter'].includes(s.name)); case 'report': return [FULL_PIPELINE.find(s => s.name === 'reporter')!]; case 'full': case 'interactive': default: return FULL_PIPELINE; } } /** * Render pipeline visualization with arrows. */ function renderPipeline(pipeline: WorkflowStep[]): string { return pipeline.map(s => { const color = AGENT_COLORS[s.name]; return color(s.name.charAt(0).toUpperCase() + s.name.slice(1)); }).join(chalk.gray(` ${figures.arrowRight} `)); } /** * Execute the full multi-agent workflow. */ export async function executeWorkflow( projectPath: string, config: SuiteConfig, mode: CliMode = 'full', ): Promise { const pipeline = getPipeline(mode); let state = createInitialState(projectPath, config); logger.header(`AI TESTING SUITE - ${mode.toUpperCase()} MODE`); logger.info(`Project: ${projectPath}`); logger.info(`Pipeline: ${renderPipeline(pipeline)}`); logger.info(`${pipeline.length} agents will be executed`); logger.newline(); const workflowStart = Date.now(); state = mergeState(state, { status: 'running' }); for (let i = 0; i < pipeline.length; i++) { const step = pipeline[i]; logger.info(`${chalk.white(`[${i + 1}/${pipeline.length}]`)} ${step.description}...`); logger.agentStart(step.name); try { state = await step.node(state); const stepDuration = Date.now() - workflowStart; // Check for critical errors after each step if (state.status === 'failed') { logger.agentError(step.name, state.errors[state.errors.length - 1]); logger.error(`Pipeline aborted after ${step.name}`); break; } logger.agentComplete(step.name, stepDuration); // Conditional edge after reviewer: retry writer if needed if (step.name === 'reviewer') { const decision = shouldRewrite(state); if (decision === 'writer') { logger.warning(`Test review failed ${figures.arrowRight} Retrying Writer Agent...`); logger.agentStart('writer'); const writerStep = FULL_PIPELINE.find(s => s.name === 'writer')!; state = await writerStep.node(state); logger.agentComplete('writer'); logger.agentStart('reviewer'); state = await step.node(state); logger.agentComplete('reviewer'); } } } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); logger.agentError(step.name, errMsg); state = mergeState(state, { errors: [...state.errors, errMsg], status: 'failed', }); break; } } const totalDuration = Date.now() - workflowStart; state = mergeState(state, { status: state.status === 'failed' ? 'failed' : 'completed', }); logger.stopSpinner(); // Completion panel const isSuccess = state.status === 'completed'; const statusIcon = isSuccess ? chalk.green(figures.tick) : chalk.red(figures.cross); const statusText = isSuccess ? chalk.green('COMPLETED') : chalk.red('FAILED'); const durationStr = `${(totalDuration / 1000).toFixed(1)}s`; const panelContent = [ `${statusIcon} Status: ${statusText}`, `${chalk.cyan(figures.pointer)} Duration: ${chalk.white(durationStr)}`, `${chalk.cyan(figures.pointer)} Agents: ${chalk.white(String(pipeline.length))}`, ]; if (state.errors.length > 0) { panelContent.push(`${chalk.yellow(figures.warning)} Errors: ${chalk.yellow(String(state.errors.length))}`); } console.log( boxen(panelContent.join('\n'), { title: 'Workflow Complete', titleAlignment: 'center', padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 1, bottom: 1, left: 2, right: 0 }, borderStyle: 'round', borderColor: isSuccess ? 'green' : 'red', }) ); return state; } /** * Execute a single agent step manually. */ export async function executeSingleAgent( agentName: AgentName, state: AgentState, ): Promise { const step = FULL_PIPELINE.find(s => s.name === agentName); if (!step) { throw new Error(`Agent '${agentName}' not found`); } logger.agentStart(step.name); const result = await step.node(state); logger.agentComplete(step.name); return result; } /** * Execute custom pipeline with selected agents. */ export async function executeCustomPipeline( agents: AgentName[], projectPath: string, config: SuiteConfig, ): Promise { let state = createInitialState(projectPath, config); const steps = agents .map(a => FULL_PIPELINE.find(s => s.name === a)) .filter(Boolean) as WorkflowStep[]; logger.header('AI TESTING SUITE - CUSTOM PIPELINE'); logger.info(`Pipeline: ${renderPipeline(steps)}`); logger.newline(); const workflowStart = Date.now(); state = mergeState(state, { status: 'running' }); for (let i = 0; i < agents.length; i++) { const agentName = agents[i]; const step = FULL_PIPELINE.find(s => s.name === agentName); if (!step) { logger.warning(`Agent '${agentName}' not found - skipped`); continue; } logger.info(`${chalk.white(`[${i + 1}/${agents.length}]`)} ${step.description}...`); logger.agentStart(step.name); try { state = await step.node(state); if (state.status === 'failed') { logger.agentError(step.name, state.errors[state.errors.length - 1]); break; } logger.agentComplete(step.name); } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); logger.agentError(step.name, errMsg); state = mergeState(state, { errors: [...state.errors, errMsg], status: 'failed', }); break; } } const totalDuration = Date.now() - workflowStart; state = mergeState(state, { status: state.status === 'failed' ? 'failed' : 'completed', }); logger.stopSpinner(); // Completion panel const isSuccess = state.status === 'completed'; const statusIcon = isSuccess ? chalk.green(figures.tick) : chalk.red(figures.cross); const statusText = isSuccess ? chalk.green('COMPLETED') : chalk.red('FAILED'); console.log( boxen( `${statusIcon} Status: ${statusText}\n${chalk.cyan(figures.pointer)} Duration: ${chalk.white(`${(totalDuration / 1000).toFixed(1)}s`)}`, { title: 'Custom Pipeline Complete', titleAlignment: 'center', padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 1, bottom: 1, left: 2, right: 0 }, borderStyle: 'round', borderColor: isSuccess ? 'green' : 'red', } ) ); return state; }