Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:17:04 +08:00
commit e758c0ab84
56 changed files with 9997 additions and 0 deletions

View 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 };

View 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 };

View 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 };

View 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 };