@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
189 lines (160 loc) ⢠5.75 kB
text/typescript
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`;
}
}