Initial commit
This commit is contained in:
198
skills/tdd-automation/scripts/generate-test.js
Executable file
198
skills/tdd-automation/scripts/generate-test.js
Executable file
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Test Template Generator
|
||||
*
|
||||
* Generates test file templates based on implementation files.
|
||||
* Analyzes project structure and test framework to create appropriate templates.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function generateTest() {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('Usage: npm run generate:test <implementation-file>');
|
||||
console.error('');
|
||||
console.error('Example:');
|
||||
console.error(' npm run generate:test src/features/auth/login.ts');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const implFile = args[0];
|
||||
|
||||
if (!fs.existsSync(implFile)) {
|
||||
console.error(`❌ Error: File not found: ${implFile}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Determine test file path
|
||||
const ext = path.extname(implFile);
|
||||
const testFile = implFile.replace(ext, `.test${ext}`);
|
||||
|
||||
if (fs.existsSync(testFile)) {
|
||||
console.error(`❌ Error: Test file already exists: ${testFile}`);
|
||||
console.error(' Use a different name or edit the existing file');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Get implementation file info
|
||||
const filename = path.basename(implFile, ext);
|
||||
const implContent = fs.readFileSync(implFile, 'utf-8');
|
||||
|
||||
// Detect test framework
|
||||
const testFramework = detectTestFramework();
|
||||
|
||||
// Generate appropriate template
|
||||
const template = generateTemplate(filename, implFile, implContent, testFramework);
|
||||
|
||||
// Write test file
|
||||
fs.writeFileSync(testFile, template, 'utf-8');
|
||||
|
||||
console.log('✅ Test file created:', testFile);
|
||||
console.log('');
|
||||
console.log('📝 Next steps:');
|
||||
console.log(' 1. Edit the test file to add specific test cases');
|
||||
console.log(' 2. Run tests to verify RED phase:');
|
||||
console.log(` npm run test:red -- ${testFile}`);
|
||||
console.log(' 3. Implement the feature');
|
||||
console.log(' 4. Run tests to verify GREEN phase:');
|
||||
console.log(` npm run test:green -- ${testFile}`);
|
||||
}
|
||||
|
||||
function detectTestFramework() {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return 'vitest'; // default
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies
|
||||
};
|
||||
|
||||
if (allDeps['vitest']) return 'vitest';
|
||||
if (allDeps['jest']) return 'jest';
|
||||
if (allDeps['@jest/globals']) return 'jest';
|
||||
if (allDeps['mocha']) return 'mocha';
|
||||
|
||||
return 'vitest'; // default
|
||||
} catch (error) {
|
||||
return 'vitest';
|
||||
}
|
||||
}
|
||||
|
||||
function generateTemplate(filename, implFile, implContent, testFramework) {
|
||||
const relativePath = path.relative(path.dirname(implFile), implFile);
|
||||
const importPath = './' + path.basename(implFile, path.extname(implFile));
|
||||
|
||||
// Extract potential functions/classes to test
|
||||
const entities = extractEntities(implContent);
|
||||
|
||||
let template = '';
|
||||
|
||||
// Add imports based on test framework
|
||||
if (testFramework === 'vitest') {
|
||||
template += `import { describe, it, expect, beforeEach, afterEach } from 'vitest';\n`;
|
||||
} else if (testFramework === 'jest') {
|
||||
template += `import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';\n`;
|
||||
} else {
|
||||
template += `const { describe, it, expect, beforeEach, afterEach } = require('${testFramework}');\n`;
|
||||
}
|
||||
|
||||
// Add import for implementation
|
||||
template += `import * as impl from '${importPath}';\n`;
|
||||
template += `\n`;
|
||||
|
||||
// Add main describe block
|
||||
template += `describe('${filename}', () => {\n`;
|
||||
template += ` // TODO: Add setup and teardown if needed\n`;
|
||||
template += ` // beforeEach(() => {\n`;
|
||||
template += ` // // Setup before each test\n`;
|
||||
template += ` // });\n`;
|
||||
template += `\n`;
|
||||
template += ` // afterEach(() => {\n`;
|
||||
template += ` // // Cleanup after each test\n`;
|
||||
template += ` // });\n`;
|
||||
template += `\n`;
|
||||
|
||||
if (entities.length > 0) {
|
||||
// Generate test stubs for each entity
|
||||
entities.forEach(entity => {
|
||||
template += ` describe('${entity}', () => {\n`;
|
||||
template += ` it('should [behavior] when [condition]', () => {\n`;
|
||||
template += ` // Arrange\n`;
|
||||
template += ` // TODO: Set up test data and dependencies\n`;
|
||||
template += `\n`;
|
||||
template += ` // Act\n`;
|
||||
template += ` // TODO: Call the function/method being tested\n`;
|
||||
template += ` // const result = impl.${entity}();\n`;
|
||||
template += `\n`;
|
||||
template += ` // Assert\n`;
|
||||
template += ` // TODO: Verify the expected behavior\n`;
|
||||
template += ` expect(true).toBe(false); // Replace with actual assertion\n`;
|
||||
template += ` });\n`;
|
||||
template += `\n`;
|
||||
template += ` // TODO: Add more test cases:\n`;
|
||||
template += ` // it('should handle edge case when [condition]', () => { ... });\n`;
|
||||
template += ` // it('should throw error when [invalid input]', () => { ... });\n`;
|
||||
template += ` });\n`;
|
||||
template += `\n`;
|
||||
});
|
||||
} else {
|
||||
// Generic test template
|
||||
template += ` it('should [behavior] when [condition]', () => {\n`;
|
||||
template += ` // Arrange\n`;
|
||||
template += ` // TODO: Set up test data and dependencies\n`;
|
||||
template += `\n`;
|
||||
template += ` // Act\n`;
|
||||
template += ` // TODO: Call the function/method being tested\n`;
|
||||
template += `\n`;
|
||||
template += ` // Assert\n`;
|
||||
template += ` // TODO: Verify the expected behavior\n`;
|
||||
template += ` expect(true).toBe(false); // Replace with actual assertion\n`;
|
||||
template += ` });\n`;
|
||||
template += `\n`;
|
||||
template += ` // TODO: Add more test cases for different scenarios\n`;
|
||||
}
|
||||
|
||||
template += `});\n`;
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
function extractEntities(content) {
|
||||
const entities = [];
|
||||
|
||||
// Extract exported functions
|
||||
const functionMatches = content.matchAll(/export\s+(?:async\s+)?function\s+([a-zA-Z0-9_]+)/g);
|
||||
for (const match of functionMatches) {
|
||||
entities.push(match[1]);
|
||||
}
|
||||
|
||||
// Extract exported classes
|
||||
const classMatches = content.matchAll(/export\s+class\s+([a-zA-Z0-9_]+)/g);
|
||||
for (const match of classMatches) {
|
||||
entities.push(match[1]);
|
||||
}
|
||||
|
||||
// Extract exported const functions (arrow functions)
|
||||
const constMatches = content.matchAll(/export\s+const\s+([a-zA-Z0-9_]+)\s*=\s*(?:async\s*)?\(/g);
|
||||
for (const match of constMatches) {
|
||||
entities.push(match[1]);
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
generateTest();
|
||||
}
|
||||
|
||||
module.exports = { generateTest };
|
||||
125
skills/tdd-automation/scripts/remove-tdd-section.js
Executable file
125
skills/tdd-automation/scripts/remove-tdd-section.js
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Section Removal Utility
|
||||
*
|
||||
* Cleanly removes only the TDD automation section from CLAUDE.md
|
||||
* while preserving all other content.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Import utilities
|
||||
const projectRoot = process.cwd();
|
||||
const skillRoot = path.join(__dirname, '..');
|
||||
|
||||
// Try to load ClaudeMdValidator
|
||||
let ClaudeMdValidator;
|
||||
try {
|
||||
ClaudeMdValidator = require(path.join(skillRoot, 'utils', 'validate-claude-md.js'));
|
||||
} catch {
|
||||
try {
|
||||
ClaudeMdValidator = require(path.join(projectRoot, '.tdd-automation', 'utils', 'validate-claude-md.js'));
|
||||
} catch {
|
||||
console.error('❌ Error: ClaudeMdValidator not found');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeTddSection() {
|
||||
console.log('🗑️ TDD Section Removal Utility\n');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const validator = new ClaudeMdValidator(projectRoot);
|
||||
|
||||
// Check if CLAUDE.md exists
|
||||
if (!fs.existsSync(validator.claudeMdPath)) {
|
||||
console.log('❌ CLAUDE.md not found');
|
||||
console.log(` Expected location: ${validator.claudeMdPath}`);
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Check if TDD section exists
|
||||
const content = fs.readFileSync(validator.claudeMdPath, 'utf-8');
|
||||
|
||||
if (!validator.detectTddSection(content)) {
|
||||
console.log('✅ No TDD section found in CLAUDE.md');
|
||||
console.log(' Nothing to remove');
|
||||
console.log('');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('🔍 TDD section detected in CLAUDE.md\n');
|
||||
|
||||
// Show what will be removed
|
||||
const startMarker = '<!-- TDD_AUTOMATION_START -->';
|
||||
const endMarker = '<!-- TDD_AUTOMATION_END -->';
|
||||
const startIdx = content.indexOf(startMarker);
|
||||
const endIdx = content.indexOf(endMarker);
|
||||
|
||||
if (startIdx !== -1 && endIdx !== -1) {
|
||||
const sectionSize = endIdx - startIdx + endMarker.length;
|
||||
console.log(` Section size: ${formatBytes(sectionSize)}`);
|
||||
console.log(` Total file size: ${formatBytes(content.length)}`);
|
||||
console.log(` After removal: ${formatBytes(content.length - sectionSize)}\n`);
|
||||
}
|
||||
|
||||
// Perform removal
|
||||
console.log('🔒 Creating backup before removal...');
|
||||
const result = validator.removeTddSection();
|
||||
|
||||
if (result.success) {
|
||||
console.log(`✅ Backup created: ${path.basename(result.backup)}\n`);
|
||||
console.log('✅ TDD section removed successfully!\n');
|
||||
console.log(' Statistics:');
|
||||
console.log(` • Original size: ${formatBytes(result.originalSize)}`);
|
||||
console.log(` • New size: ${formatBytes(result.newSize)}`);
|
||||
console.log(` • Removed: ${formatBytes(result.removed)}\n`);
|
||||
console.log(' Backup available at:');
|
||||
console.log(` ${result.backup}\n`);
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
console.log('⚠️ TDD automation configuration removed from CLAUDE.md\n');
|
||||
console.log(' Other TDD automation components remain:');
|
||||
console.log(' • .tdd-automation/ directory');
|
||||
console.log(' • npm scripts (test:tdd, validate:tdd, etc.)');
|
||||
console.log(' • git hooks (.git/hooks/pre-commit)');
|
||||
console.log(' • Claude hooks (.claude/hooks/tdd-auto-enforcer.sh)\n');
|
||||
console.log(' To remove all components:');
|
||||
console.log(' node .tdd-automation/scripts/uninstall-tdd.js\n');
|
||||
console.log(' To restore this section:');
|
||||
console.log(` node .tdd-automation/scripts/rollback-tdd.js\n`);
|
||||
} else {
|
||||
console.error('❌ Removal failed:', result.reason);
|
||||
console.error('');
|
||||
console.error(' Possible issues:');
|
||||
console.error(' • Section markers not found (manual edit may be needed)');
|
||||
console.error(' • File permissions issue');
|
||||
console.error(' • Disk space issue');
|
||||
console.error('');
|
||||
console.error(' Manual removal:');
|
||||
console.error(' 1. Open .claude/CLAUDE.md in editor');
|
||||
console.error(' 2. Find <!-- TDD_AUTOMATION_START -->');
|
||||
console.error(' 3. Delete everything until <!-- TDD_AUTOMATION_END -->');
|
||||
console.error(' 4. Save file');
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return `${bytes} bytes`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
removeTddSection().catch(error => {
|
||||
console.error('❌ Unexpected error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { removeTddSection };
|
||||
141
skills/tdd-automation/scripts/rollback-tdd.js
Executable file
141
skills/tdd-automation/scripts/rollback-tdd.js
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Automation Rollback Utility
|
||||
*
|
||||
* Restores previous CLAUDE.md from backup and optionally removes all TDD automation.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Import utilities from parent directory
|
||||
const projectRoot = process.cwd();
|
||||
const skillRoot = path.join(__dirname, '..');
|
||||
|
||||
// Try to load ClaudeMdValidator from skill location
|
||||
let ClaudeMdValidator;
|
||||
try {
|
||||
ClaudeMdValidator = require(path.join(skillRoot, 'utils', 'validate-claude-md.js'));
|
||||
} catch {
|
||||
// If skill utils not available, use local copy in .tdd-automation
|
||||
try {
|
||||
ClaudeMdValidator = require(path.join(projectRoot, '.tdd-automation', 'utils', 'validate-claude-md.js'));
|
||||
} catch {
|
||||
console.error('❌ Error: ClaudeMdValidator not found');
|
||||
console.error(' This script must be run from project root or .tdd-automation directory');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
async function rollbackTddAutomation() {
|
||||
console.log('🔄 TDD Automation Rollback Utility\n');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const validator = new ClaudeMdValidator(projectRoot);
|
||||
|
||||
// List available backups
|
||||
const backups = validator.listBackups();
|
||||
|
||||
if (backups.length === 0) {
|
||||
console.log('❌ No backup files found');
|
||||
console.log(' Cannot rollback without backup');
|
||||
console.log('');
|
||||
console.log(' Backup files should be in: .claude/CLAUDE.md.backup.*');
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`📋 Found ${backups.length} backup(s):\n`);
|
||||
|
||||
backups.forEach((backup, i) => {
|
||||
const age = getTimeAgo(backup.created);
|
||||
console.log(`${i + 1}. ${backup.name}`);
|
||||
console.log(` Created: ${backup.created.toISOString()} (${age})`);
|
||||
console.log(` Size: ${formatBytes(backup.size)}\n`);
|
||||
});
|
||||
|
||||
// Use most recent backup
|
||||
const latestBackup = backups[0];
|
||||
console.log(`🔄 Rolling back to most recent backup:\n`);
|
||||
console.log(` ${latestBackup.name}`);
|
||||
console.log(` Created: ${latestBackup.created.toISOString()}\n`);
|
||||
|
||||
// Create backup of current state before rollback
|
||||
console.log('🔒 Creating safety backup of current state...');
|
||||
const currentBackup = validator.createBackup();
|
||||
|
||||
if (currentBackup.success) {
|
||||
console.log(`✅ Current state backed up to:`);
|
||||
console.log(` ${path.basename(currentBackup.path)}\n`);
|
||||
} else {
|
||||
console.log(`⚠️ Could not backup current state: ${currentBackup.reason}`);
|
||||
console.log(` Proceeding with rollback anyway...\n`);
|
||||
}
|
||||
|
||||
// Perform rollback
|
||||
console.log('🔄 Performing rollback...');
|
||||
const result = validator.rollback(latestBackup.path);
|
||||
|
||||
if (result.success) {
|
||||
console.log('✅ Rollback successful!\n');
|
||||
console.log(' CLAUDE.md has been restored from:');
|
||||
console.log(` ${path.basename(result.restoredFrom)}`);
|
||||
console.log(` Size: ${formatBytes(result.size)}\n`);
|
||||
|
||||
if (currentBackup.success) {
|
||||
console.log(' Your previous state was saved to:');
|
||||
console.log(` ${path.basename(currentBackup.path)}\n`);
|
||||
}
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
console.log('⚠️ TDD automation configuration has been removed from CLAUDE.md\n');
|
||||
console.log(' Other TDD automation components remain:');
|
||||
console.log(' • .tdd-automation/ directory');
|
||||
console.log(' • npm scripts (test:tdd, validate:tdd, etc.)');
|
||||
console.log(' • git hooks (.git/hooks/pre-commit)');
|
||||
console.log(' • Claude hooks (.claude/hooks/tdd-auto-enforcer.sh)\n');
|
||||
console.log(' To remove all TDD automation components:');
|
||||
console.log(' node .tdd-automation/scripts/uninstall-tdd.js\n');
|
||||
console.log(' To reinstall TDD automation:');
|
||||
console.log(' Run the tdd-automation skill again\n');
|
||||
} else {
|
||||
console.error('❌ Rollback failed:', result.reason);
|
||||
console.error('');
|
||||
console.error(' You may need to manually restore CLAUDE.md from backup');
|
||||
console.error(` Backup location: ${latestBackup.path}`);
|
||||
console.error('');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeAgo(date) {
|
||||
const now = new Date();
|
||||
const diff = now - date;
|
||||
|
||||
const seconds = Math.floor(diff / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const days = Math.floor(hours / 24);
|
||||
|
||||
if (days > 0) return `${days} day${days > 1 ? 's' : ''} ago`;
|
||||
if (hours > 0) return `${hours} hour${hours > 1 ? 's' : ''} ago`;
|
||||
if (minutes > 0) return `${minutes} minute${minutes > 1 ? 's' : ''} ago`;
|
||||
return `${seconds} second${seconds > 1 ? 's' : ''} ago`;
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes < 1024) return `${bytes} bytes`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
rollbackTddAutomation().catch(error => {
|
||||
console.error('❌ Unexpected error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { rollbackTddAutomation };
|
||||
300
skills/tdd-automation/scripts/validate-tdd.js
Executable file
300
skills/tdd-automation/scripts/validate-tdd.js
Executable file
@@ -0,0 +1,300 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* TDD Compliance Validator
|
||||
*
|
||||
* Validates that project follows TDD best practices:
|
||||
* - All implementation files have corresponding tests
|
||||
* - Test coverage meets minimum threshold
|
||||
* - Tests are properly structured
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
async function validateTdd() {
|
||||
console.log('🔍 TDD Compliance Validation\n');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
const results = {
|
||||
passed: [],
|
||||
failed: [],
|
||||
warnings: []
|
||||
};
|
||||
|
||||
// Check 1: Verify test files exist for implementation files
|
||||
console.log('📋 Check 1: Test file coverage...');
|
||||
await checkTestFileCoverage(results);
|
||||
|
||||
// Check 2: Verify test framework is installed
|
||||
console.log('\n📋 Check 2: Test framework installation...');
|
||||
await checkTestFramework(results);
|
||||
|
||||
// Check 3: Verify TDD npm scripts exist
|
||||
console.log('\n📋 Check 3: TDD npm scripts...');
|
||||
await checkNpmScripts(results);
|
||||
|
||||
// Check 4: Verify git hooks installed
|
||||
console.log('\n📋 Check 4: Git hooks...');
|
||||
await checkGitHooks(results);
|
||||
|
||||
// Check 5: Verify CLAUDE.md has TDD configuration
|
||||
console.log('\n📋 Check 5: CLAUDE.md configuration...');
|
||||
await checkClaudeMd(results);
|
||||
|
||||
// Summary
|
||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
console.log('📊 Validation Summary\n');
|
||||
|
||||
if (results.passed.length > 0) {
|
||||
console.log(`✅ Passed: ${results.passed.length}`);
|
||||
results.passed.forEach(msg => console.log(` • ${msg}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (results.warnings.length > 0) {
|
||||
console.log(`⚠️ Warnings: ${results.warnings.length}`);
|
||||
results.warnings.forEach(msg => console.log(` • ${msg}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
if (results.failed.length > 0) {
|
||||
console.log(`❌ Failed: ${results.failed.length}`);
|
||||
results.failed.forEach(msg => console.log(` • ${msg}`));
|
||||
console.log('');
|
||||
}
|
||||
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
||||
|
||||
if (results.failed.length > 0) {
|
||||
console.log('❌ TDD compliance validation FAILED\n');
|
||||
console.log(' Please address the failed checks above');
|
||||
console.log(' Run: npm run generate:test <file> to create missing tests\n');
|
||||
process.exit(1);
|
||||
} else if (results.warnings.length > 0) {
|
||||
console.log('⚠️ TDD compliance validation PASSED with warnings\n');
|
||||
console.log(' Consider addressing the warnings above\n');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('✅ TDD compliance validation PASSED\n');
|
||||
console.log(' All checks passed successfully!\n');
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTestFileCoverage(results) {
|
||||
const srcDir = path.join(process.cwd(), 'src');
|
||||
|
||||
if (!fs.existsSync(srcDir)) {
|
||||
results.warnings.push('No src/ directory found - skipping test coverage check');
|
||||
return;
|
||||
}
|
||||
|
||||
const implFiles = findImplementationFiles(srcDir);
|
||||
const testFiles = findTestFiles(srcDir);
|
||||
|
||||
let missing = 0;
|
||||
let covered = 0;
|
||||
|
||||
for (const implFile of implFiles) {
|
||||
const testFile = findCorrespondingTest(implFile, testFiles);
|
||||
|
||||
if (!testFile) {
|
||||
missing++;
|
||||
if (missing <= 5) { // Only show first 5 to avoid spam
|
||||
results.failed.push(`Missing test for: ${path.relative(process.cwd(), implFile)}`);
|
||||
}
|
||||
} else {
|
||||
covered++;
|
||||
}
|
||||
}
|
||||
|
||||
if (missing > 5) {
|
||||
results.failed.push(`... and ${missing - 5} more files without tests`);
|
||||
}
|
||||
|
||||
const totalFiles = implFiles.length;
|
||||
const coverage = totalFiles > 0 ? ((covered / totalFiles) * 100).toFixed(1) : 100;
|
||||
|
||||
if (missing === 0 && totalFiles > 0) {
|
||||
results.passed.push(`All ${totalFiles} implementation files have tests (100%)`);
|
||||
} else if (coverage >= 80) {
|
||||
results.warnings.push(`Test file coverage: ${coverage}% (${covered}/${totalFiles}) - below 100%`);
|
||||
} else if (totalFiles === 0) {
|
||||
results.warnings.push('No implementation files found in src/');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTestFramework(results) {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
results.failed.push('package.json not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies
|
||||
};
|
||||
|
||||
const testFrameworks = ['vitest', 'jest', '@jest/globals', 'mocha', 'ava'];
|
||||
const installed = testFrameworks.find(fw => allDeps[fw]);
|
||||
|
||||
if (installed) {
|
||||
results.passed.push(`Test framework installed: ${installed}`);
|
||||
} else {
|
||||
results.failed.push('No test framework found (install vitest, jest, or mocha)');
|
||||
}
|
||||
} catch (error) {
|
||||
results.failed.push(`Error reading package.json: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNpmScripts(results) {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return; // Already reported in previous check
|
||||
}
|
||||
|
||||
try {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
||||
const scripts = packageJson.scripts || {};
|
||||
|
||||
const requiredScripts = ['test:tdd', 'validate:tdd', 'generate:test'];
|
||||
const missingScripts = requiredScripts.filter(script => !scripts[script]);
|
||||
|
||||
if (missingScripts.length === 0) {
|
||||
results.passed.push('All TDD npm scripts installed');
|
||||
} else {
|
||||
missingScripts.forEach(script => {
|
||||
results.warnings.push(`Missing npm script: ${script}`);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
results.failed.push(`Error checking npm scripts: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkGitHooks(results) {
|
||||
const preCommitPath = path.join(process.cwd(), '.git', 'hooks', 'pre-commit');
|
||||
|
||||
if (!fs.existsSync(path.join(process.cwd(), '.git'))) {
|
||||
results.warnings.push('Not a git repository - no git hooks checked');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(preCommitPath)) {
|
||||
results.warnings.push('Git pre-commit hook not installed');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(preCommitPath, 'utf-8');
|
||||
|
||||
if (content.includes('TDD_AUTOMATION') || content.includes('TDD Validation')) {
|
||||
results.passed.push('Git pre-commit hook installed for TDD validation');
|
||||
} else {
|
||||
results.warnings.push('Git pre-commit hook exists but may not have TDD validation');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkClaudeMd(results) {
|
||||
const claudeMdPath = path.join(process.cwd(), '.claude', 'CLAUDE.md');
|
||||
|
||||
if (!fs.existsSync(claudeMdPath)) {
|
||||
results.warnings.push('No .claude/CLAUDE.md found');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(claudeMdPath, 'utf-8');
|
||||
|
||||
const tddMarkers = [
|
||||
'TDD Red-Green-Refactor',
|
||||
'tdd-automation-version',
|
||||
'<!-- TDD_AUTOMATION_START -->'
|
||||
];
|
||||
|
||||
const hasTddConfig = tddMarkers.some(marker => content.includes(marker));
|
||||
|
||||
if (hasTddConfig) {
|
||||
results.passed.push('CLAUDE.md has TDD configuration');
|
||||
} else {
|
||||
results.warnings.push('CLAUDE.md exists but missing TDD configuration');
|
||||
}
|
||||
}
|
||||
|
||||
function findImplementationFiles(dir, files = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
// Skip node_modules, dist, build, etc.
|
||||
if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
||||
findImplementationFiles(fullPath, files);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
// Include .ts, .js, .tsx, .jsx files but exclude test files
|
||||
if (/\.(ts|js|tsx|jsx)$/.test(entry.name) && !/\.(test|spec)\./.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function findTestFiles(dir, files = []) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
if (!['node_modules', 'dist', 'build', '.git', 'coverage'].includes(entry.name)) {
|
||||
findTestFiles(fullPath, files);
|
||||
}
|
||||
} else if (entry.isFile()) {
|
||||
if (/\.(test|spec)\.(ts|js|tsx|jsx)$/.test(entry.name)) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function findCorrespondingTest(implFile, testFiles) {
|
||||
const dir = path.dirname(implFile);
|
||||
const filename = path.basename(implFile);
|
||||
const base = filename.replace(/\.(ts|js|tsx|jsx)$/, '');
|
||||
|
||||
// Try multiple test file patterns
|
||||
const patterns = [
|
||||
path.join(dir, `${base}.test.ts`),
|
||||
path.join(dir, `${base}.test.js`),
|
||||
path.join(dir, `${base}.test.tsx`),
|
||||
path.join(dir, `${base}.test.jsx`),
|
||||
path.join(dir, `${base}.spec.ts`),
|
||||
path.join(dir, `${base}.spec.js`),
|
||||
path.join(dir, `${base}.spec.tsx`),
|
||||
path.join(dir, `${base}.spec.jsx`)
|
||||
];
|
||||
|
||||
return testFiles.find(testFile => patterns.includes(testFile));
|
||||
}
|
||||
|
||||
// Run if called directly
|
||||
if (require.main === module) {
|
||||
validateTdd().catch(error => {
|
||||
console.error('❌ Unexpected error:', error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { validateTdd };
|
||||
Reference in New Issue
Block a user