UNPKG

@morodomi/ait3

Version:

AIT³ Development Platform - AI + Ticket + Test + Tool driven development methodology

177 lines (162 loc) 7.47 kB
import { ValidationError } from '../../common/errors.js'; import { STYLES } from '../../common/styles.js'; import { FLOW_MESSAGES } from '../../common/flow-messages.js'; import { SlugUtils, IDUtils } from '../../common/utils.js'; import { generateCommitMessage, formatTicketHeader, getTicketOrThrow } from '../../common/flow-utils.js'; import { formatTicketDisplay } from '../../common/utils/location-utils.js'; export async function redPhase(args, services) { // Validate ticket ID if (!args.ticketId || args.ticketId.trim() === '') { throw new ValidationError(FLOW_MESSAGES.TICKET_ID_REQUIRED('RED'), 'ticketId'); } // Validate ID format if (!IDUtils.isValidTicketId(args.ticketId)) { throw new ValidationError('Invalid ticket ID format. Use local format (0001) or GitHub format (#70, 70)', 'ticketId'); } const { ticketId, type = 'unit', interactive = false, dryRun = false } = args; // Get ticket information const ticket = await getTicketOrThrow(ticketId, services); // Check ticket status if (ticket.status === 'done') { throw new ValidationError(FLOW_MESSAGES.TICKET_ALREADY_COMPLETED(ticketId)); } // Generate warning for tickets already in progress let statusWarning = ''; if (ticket.status === 'doing') { statusWarning = `\n${STYLES.warning('WARNING: Warning: Ticket is already in progress')}`; } // Parse acceptance criteria from ticket description const testCases = extractTestCases(ticket); const hasApiLabel = ticket.labels?.includes('api') || ticket.labels?.includes('rest'); // Handle interactive mode if (interactive) { return generateInteractiveOutput(ticket, testCases, dryRun, services); } // Handle dry run mode if (dryRun) { return generateDryRunOutput(ticket, type, testCases, services); } // Generate test files based on type const results = []; if (type === 'unit' || type === 'both') { const unitPath = generateUnitTestPath(ticket); results.push(`${STYLES.success('INFO:')} Unit test generated: ${STYLES.info(unitPath)}`); } if (type === 'integration' || type === 'both') { const integrationPath = generateIntegrationTestPath(ticket); results.push(`${STYLES.success('INFO:')} Integration test generated: ${STYLES.info(integrationPath)}`); } // Add test case count const testCount = testCases.length > 0 ? testCases.length : 3; // Default 3 basic tests results.push(`${STYLES.info('INFO:')} ${STYLES.info(testCount + ' test cases generated')}${testCases.length > 0 ? ' from acceptance criteria' : ''}`); // Add API pattern note if applicable if (hasApiLabel) { results.push(`${STYLES.info('INFO:')} API test pattern applied`); } // Add pass rate results.push(`${STYLES.danger('INFO:')} Current pass rate: ${STYLES.danger('0% pass rate')} ${STYLES.muted('(all tests failing as expected)')}`); // Get ticket title for better formatting const ticketTitle = ticket.title || 'Feature'; const ticketLocation = formatTicketDisplay(ticket, services.ticketService); return { success: true, message: ` ${formatTicketHeader(ticketId, ticketTitle, 'RED Phase', ticket, services)}${statusWarning} ${STYLES.bold('Claude Code Instructions')}: 1. Read ticket: ${STYLES.info(ticketLocation)} 2. Create comprehensive test cases: ├─ Test behavior, not implementation ├─ Cover all acceptance criteria ├─ Include edge cases & error scenarios └─ Ensure 0% pass rate initially 3. Test coverage check: - Not required to be 100% - Must be sufficient for the feature - Verify all critical paths covered ${STYLES.bold('LOCATION: Test Locations')}: ├─ Unit: ${STYLES.info(`src/commands/${ticket.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.test.ts`)} └─ Integration: ${STYLES.info(`tests/integration/${ticket.title.toLowerCase().replace(/[^a-z0-9]+/g, '-')}.test.ts`)} ${results.join('\n')} ${STYLES.info('TODO: Next actions for AI')}: ├─ Read acceptance criteria: │ └─ Analyze ${STYLES.info(ticketLocation)} ├─ Create test files: │ └─ Implement failing tests for all scenarios ├─ Verify 0% pass rate: │ └─ Run tests to confirm all failing └─ Commit test suite: └─ ${STYLES.code(`git commit -m "${generateCommitMessage('test', ticketId, ticketTitle)}"`)}, ${STYLES.muted('After test creation: ait3 flow green')} ` }; } function extractTestCases(ticket) { const testCases = []; // Look for acceptance criteria in description if (ticket.description) { const acceptanceCriteriaMatch = ticket.description.match(/## Acceptance Criteria\n([\s\S]*?)(?=\n##|$)/); if (acceptanceCriteriaMatch) { const criteriaText = acceptanceCriteriaMatch[1]; const criteriaLines = criteriaText.split('\n').filter((line) => line.trim().startsWith('- [ ]')); testCases.push(...criteriaLines.map((line) => line.replace('- [ ]', '').trim())); } } return testCases; } function generateUnitTestPath(ticket) { const featureName = SlugUtils.titleToSlug(ticket.title); return `tests/commands/flow/${featureName}.test.ts`; } function generateIntegrationTestPath(ticket) { const featureName = SlugUtils.titleToSlug(ticket.title); return `tests/integration/cli/flow/${featureName}.integration.test.ts`; } function generateInteractiveOutput(ticket, testCases, dryRun, services) { const prefix = dryRun ? `${STYLES.warning('[DRY RUN]')} ` : ''; const ticketLocation = formatTicketDisplay(ticket, services.ticketService); return { success: true, message: ` ${prefix}${formatTicketHeader(ticket.id, ticket.title, 'RED Phase', ticket, services)} ${STYLES.bold('Claude Code Instructions')}: 1. Read ticket: ${STYLES.info(ticketLocation)} 2. Test cases identified: ${testCases.length || 3} 3. Interactive mode options: ${STYLES.bold('Choose test generation strategy')}: ${STYLES.info('[1]')} Generate all test cases automatically ${STYLES.info('[2]')} Review and customize test cases ${STYLES.info('[3]')} Add additional edge cases ${STYLES.info('[4]')} Skip and write tests manually ${STYLES.info('TODO: Next actions for AI')}: └─ Choose strategy and proceed with test creation ${STYLES.muted('Interactive mode: Select an option to continue')} ` }; } function generateDryRunOutput(ticket, type, testCases, services) { const files = []; const ticketLocation = formatTicketDisplay(ticket, services.ticketService); if (type === 'unit' || type === 'both') { files.push(`- ${generateUnitTestPath(ticket)}`); } if (type === 'integration' || type === 'both') { files.push(`- ${generateIntegrationTestPath(ticket)}`); } return { success: true, message: ` ${STYLES.warning('INFO: DRY RUN')} - Preview mode for Ticket #${ticket.id}: ${ticket.title} ${STYLES.info('LOCATION: Ticket location')}: ${STYLES.info(ticketLocation)} ${STYLES.bold('INFO: Would execute')}: 1. Read ticket: ${STYLES.info(ticketLocation)} 2. Generate test files: ${files.join('\n')} ${STYLES.info('Test cases')}: ${testCases.length || 3} ${STYLES.info('Test type')}: ${type} ${STYLES.info('Expected pass rate')}: 0% ${STYLES.info('TODO: Next actions for AI')}: └─ Run without --dry-run flag to create test files ${STYLES.muted('Dry run mode: Preview only')} ` }; }