UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

189 lines (160 loc) • 5.75 kB
import { Feature, TestCase, TDDSession, TDDPhaseTransition } from './types.js'; export class TDDEnforcer { private sessions: Map<string, TDDSession> = new Map(); startTDDSession(feature: Feature): TDDSession { if (feature.testCases.length === 0) { throw new Error( '🚫 TDD Violation: Cannot start development without tests!\n' + 'šŸ“ Please write at least one failing test before implementation.\n' + 'Use the "write_test" tool to create your first test.' ); } const failingTests = feature.testCases.filter(tc => tc.status !== 'passing'); if (failingTests.length === 0) { throw new Error( '🚫 TDD Violation: All tests are already passing!\n' + 'šŸ”“ TDD requires starting with a failing test (RED phase).\n' + 'Write a new test for the next behavior you want to implement.' ); } const session: TDDSession = { id: this.generateId(), feature, currentPhase: 'red', history: [{ from: 'red', to: 'red', timestamp: new Date(), message: `Started TDD session with ${failingTests.length} failing test(s)`, }], startedAt: new Date(), }; this.sessions.set(session.id, session); return session; } canImplementCode(sessionId: string): { allowed: boolean; message: string } { const session = this.sessions.get(sessionId); if (!session) { return { allowed: false, message: '🚫 No active TDD session. Use "start_tdd_session" first.', }; } if (session.currentPhase !== 'red') { return { allowed: false, message: `🚫 You're in the ${session.currentPhase.toUpperCase()} phase. You can only write implementation code in the RED phase.`, }; } const failingTests = session.feature.testCases.filter(tc => tc.status === 'failing'); if (failingTests.length === 0) { return { allowed: false, message: '🚫 No failing tests found. Write a failing test first!', }; } return { allowed: true, message: `āœ… You have ${failingTests.length} failing test(s). You may now implement code to make them pass.`, }; } transitionToGreen(sessionId: string, passingTests: string[]): void { const session = this.sessions.get(sessionId); if (!session) { throw new Error('No active TDD session'); } if (session.currentPhase !== 'red') { throw new Error(`Cannot transition to GREEN from ${session.currentPhase} phase`); } session.currentPhase = 'green'; session.history.push({ from: 'red', to: 'green', timestamp: new Date(), message: `Made ${passingTests.length} test(s) pass`, }); } canRefactor(sessionId: string): { allowed: boolean; message: string } { const session = this.sessions.get(sessionId); if (!session) { return { allowed: false, message: '🚫 No active TDD session.', }; } if (session.currentPhase !== 'green') { return { allowed: false, message: `🚫 You can only refactor in the GREEN phase. Current phase: ${session.currentPhase.toUpperCase()}`, }; } const allTestsPassing = session.feature.testCases.every(tc => tc.status === 'passing'); if (!allTestsPassing) { return { allowed: false, message: '🚫 Not all tests are passing. Fix failing tests before refactoring.', }; } return { allowed: true, message: 'āœ… All tests passing. You may now refactor the code.', }; } completeRefactoring(sessionId: string): void { const session = this.sessions.get(sessionId); if (!session) { throw new Error('No active TDD session'); } if (session.currentPhase !== 'green') { throw new Error(`Cannot complete refactoring from ${session.currentPhase} phase`); } session.currentPhase = 'refactor'; session.history.push({ from: 'green', to: 'refactor', timestamp: new Date(), message: 'Completed refactoring while maintaining all tests passing', }); } startNewCycle(sessionId: string): void { const session = this.sessions.get(sessionId); if (!session) { throw new Error('No active TDD session'); } session.currentPhase = 'red'; session.history.push({ from: 'refactor', to: 'red', timestamp: new Date(), message: 'Started new TDD cycle', }); } getSessionReport(sessionId: string): string { const session = this.sessions.get(sessionId); if (!session) { return 'āŒ No active TDD session found'; } const duration = this.formatDuration(session.startedAt, session.completedAt || new Date()); const cycleCount = session.history.filter(h => h.from === 'refactor' && h.to === 'red').length + 1; return ` šŸ”„ TDD Session Report ━━━━━━━━━━━━━━━━━━━━ šŸ“‹ Feature: ${session.feature.name} ā±ļø Duration: ${duration} šŸ” Cycles Completed: ${cycleCount} šŸ“Š Current Phase: ${session.currentPhase.toUpperCase()} āœ… Tests Passing: ${session.feature.testCases.filter(tc => tc.status === 'passing').length}/${session.feature.testCases.length} šŸ“œ History: ${session.history.map(h => ` • ${h.message} (${h.timestamp.toLocaleTimeString()})`).join('\n')} `; } private generateId(): string { return Date.now().toString(36) + Math.random().toString(36).substr(2); } private formatDuration(start: Date, end: Date): string { const ms = end.getTime() - start.getTime(); const minutes = Math.floor(ms / 60000); const seconds = Math.floor((ms % 60000) / 1000); return `${minutes}m ${seconds}s`; } }