release-readiness-detector.test.ts•22.4 kB
/**
* Test suite for release readiness detector
*/
import { describe, it, expect, beforeEach, afterEach, jest } from '@jest/globals';
import { join } from 'path';
// Mock file system operations
const mockReadFileSync = jest.fn();
const mockWriteFileSync = jest.fn();
const mockMkdirSync = jest.fn();
const mockRmSync = jest.fn();
const mockExistsSync = jest.fn();
const mockStatSync = jest.fn();
jest.unstable_mockModule('fs', () => ({
readFileSync: mockReadFileSync,
writeFileSync: mockWriteFileSync,
mkdirSync: mockMkdirSync,
rmSync: mockRmSync,
existsSync: mockExistsSync,
statSync: mockStatSync,
}));
// Mock child_process for git commands
const mockExecSync = jest.fn();
jest.unstable_mockModule('child_process', () => ({
execSync: mockExecSync,
}));
describe('Release Readiness Detector', () => {
let testDir: string;
beforeEach(() => {
testDir = '/tmp/test-release-readiness';
jest.clearAllMocks();
// Setup default mocks
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue('');
mockExecSync.mockReturnValue('');
mockStatSync.mockReturnValue({ size: 1024 } as any);
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('TODO.md Analysis', () => {
it('should parse TODO.md sections correctly', async () => {
const todoContent = `# Project TODO
## Core Features
- [x] Implement user authentication
- [x] Add database integration
- [ ] Create REST API endpoints (high priority)
- [ ] Add input validation
## Testing
- [x] Unit tests for auth
- [ ] Integration tests (critical)
- [ ] E2E tests
## Documentation
- [ ] API documentation
- [ ] User guide (low priority)
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
todoPath: join(testDir, 'TODO.md'),
});
expect(result.milestones).toHaveLength(3);
// Check Core Features milestone
const coreFeatures = result.milestones.find(m => m.name === 'Core Features');
expect(coreFeatures).toBeDefined();
expect(coreFeatures!.total).toBe(4);
expect(coreFeatures!.completed).toBe(2);
expect(coreFeatures!.completionRate).toBe(0.5);
expect(coreFeatures!.criticalTodos).toBe(1); // high priority item
// Check Testing milestone
const testing = result.milestones.find(m => m.name === 'Testing');
expect(testing).toBeDefined();
expect(testing!.total).toBe(3);
expect(testing!.completed).toBe(1);
expect(testing!.criticalTodos).toBe(1); // critical item
});
it('should detect critical TODO items correctly', async () => {
const todoContent = `# Project TODO
## Release Blockers
- [ ] Fix security vulnerability (critical)
- [ ] Resolve performance issue (high priority)
- [ ] Update dependencies (blocker)
- [ ] Add logging (medium priority)
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
maxCriticalTodos: 0,
});
expect(result.isReady).toBe(false);
expect(result.milestones[0]?.criticalTodos).toBe(3); // critical, high, blocker
const criticalBlockers = result.blockers.filter(b => b.type === 'critical-todos');
expect(criticalBlockers).toHaveLength(1);
expect(criticalBlockers[0]?.severity).toBe('error');
});
it('should handle empty TODO.md correctly', async () => {
const todoContent = `# Project TODO
No TODO items yet.
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
expect(result.milestones).toHaveLength(0);
expect(result.isReady).toBe(true); // No TODOs means ready
expect(result.score).toBe(1.0);
});
it('should handle missing TODO.md file', async () => {
mockExistsSync.mockReturnValue(false);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
expect(result.milestones).toHaveLength(0);
expect(result.isReady).toBe(true); // No TODO file means ready
expect(result.score).toBe(1.0);
});
});
describe('Completion Rate Analysis', () => {
it('should calculate completion rates correctly', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
- [x] Task 3
- [x] Task 4
- [ ] Task 5
## Feature B
- [x] Task 1
- [ ] Task 2
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
minCompletionRate: 0.8,
});
// Feature A: 4/5 = 0.8 (80%)
const featureA = result.milestones.find(m => m.name === 'Feature A');
expect(featureA!.completionRate).toBe(0.8);
// Feature B: 1/2 = 0.5 (50%)
const featureB = result.milestones.find(m => m.name === 'Feature B');
expect(featureB!.completionRate).toBe(0.5);
// Overall should not be ready due to Feature B
expect(result.isReady).toBe(false);
});
it('should apply different completion thresholds', async () => {
const todoContent = `# Project TODO
## Feature
- [x] Task 1
- [x] Task 2
- [x] Task 3
- [ ] Task 4
- [ ] Task 5
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
// Test with 60% threshold
const result60 = await analyzeReleaseReadiness({
projectPath: testDir,
minCompletionRate: 0.6,
});
expect(result60.isReady).toBe(true); // 3/5 = 0.6, meets threshold
// Test with 80% threshold
const result80 = await analyzeReleaseReadiness({
projectPath: testDir,
minCompletionRate: 0.8,
});
expect(result80.isReady).toBe(false); // 3/5 = 0.6, doesn't meet threshold
});
});
describe('Release Scoring Algorithm', () => {
it('should score perfect completion correctly', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
## Feature B
- [x] Task 1
- [x] Task 2
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
mockExecSync.mockReturnValue(''); // Clean git status
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
expect(result.score).toBe(1.0);
expect(result.isReady).toBe(true);
expect(result.confidence).toBeGreaterThan(0.8);
});
it('should penalize critical todos heavily', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
- [x] Task 3
- [x] Task 4
- [ ] Critical bug fix (critical)
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
maxCriticalTodos: 0,
});
expect(result.score).toBeLessThan(0.5); // Should be heavily penalized
expect(result.isReady).toBe(false);
});
it('should factor in different severity blockers', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
- [x] Task 3
- [x] Task 4
- [ ] Minor improvement
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
// Mock git commands to simulate warnings
mockExecSync
.mockReturnValueOnce('M file1.js\n?? debug_temp.js') // Uncommitted changes
.mockReturnValueOnce('abc123 WIP: temp fix\ndef456 debug: testing'); // Concerning commits
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
// Should have warning-level blockers but still be mostly ready
expect(result.score).toBeLessThan(1.0);
expect(result.score).toBeGreaterThan(0.6);
const warningBlockers = result.blockers.filter(b => b.severity === 'warning');
expect(warningBlockers.length).toBeGreaterThan(0);
});
});
describe('Git History Analysis', () => {
it('should detect concerning commit patterns', async () => {
mockExistsSync.mockReturnValue(false); // No TODO.md
mockExecSync
.mockReturnValueOnce(
'abc123 debug: temp fix\ndef456 WIP: work in progress\n789abc fixme: quick hack'
) // Recent commits
.mockReturnValueOnce(''); // Clean working directory
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
const unstableBlockers = result.blockers.filter(b => b.type === 'unstable-code');
expect(unstableBlockers.length).toBeGreaterThan(0);
expect(unstableBlockers[0]?.severity).toBe('warning');
expect(unstableBlockers[0]?.message).toContain('concerning patterns');
});
it('should detect uncommitted changes', async () => {
mockExistsSync.mockReturnValue(false); // No TODO.md
mockExecSync
.mockReturnValueOnce('abc123 feat: add new feature') // Clean commits
.mockReturnValueOnce('M file1.js\n?? file2.js'); // Uncommitted changes
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
const unstableBlockers = result.blockers.filter(b => b.type === 'unstable-code');
expect(unstableBlockers.some(b => b.message.includes('Uncommitted changes'))).toBe(true);
});
it('should handle git command failures gracefully', async () => {
mockExistsSync.mockReturnValue(false); // No TODO.md
mockExecSync.mockImplementation(() => {
throw new Error('git: command not found');
});
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
// Should still work but with info-level blocker
expect(result.blockers.some(b => b.severity === 'info')).toBe(true);
expect(result.score).toBeGreaterThan(0); // Should not completely fail
});
});
describe('Project State Analysis', () => {
it('should detect test configuration', async () => {
mockExistsSync.mockImplementation((path: any) => {
if (String(path).includes('TODO.md')) return false;
if (String(path).includes('package.json')) return true;
return false;
});
const packageJsonContent = JSON.stringify({
name: 'test-project',
scripts: {
test: 'jest',
build: 'tsc',
},
});
mockReadFileSync.mockReturnValue(packageJsonContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
const testBlockers = result.blockers.filter(b => b.type === 'test-failures');
expect(testBlockers.length).toBeGreaterThan(0);
expect(testBlockers[0]?.message).toContain('Tests should be run');
});
it('should handle missing package.json', async () => {
mockExistsSync.mockReturnValue(false);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
// Should work without package.json
expect(result.score).toBeGreaterThan(0);
});
});
describe('Recommendation Generation', () => {
it('should provide appropriate recommendations for ready projects', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
- [x] Task 3
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
mockExecSync.mockReturnValue(''); // Clean git
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
expect(result.recommendations).toContain('✅ Project appears ready for release');
expect(result.recommendations).toContain('🚀 Consider creating a release candidate');
});
it('should provide specific recommendations for blockers', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [ ] Critical bug fix (critical)
- [ ] Performance optimization (high priority)
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
maxCriticalTodos: 0,
});
expect(result.recommendations).toContain('❌ Project is not ready for release');
expect(result.recommendations).toContain('🚨 Critical blockers must be resolved:');
expect(result.recommendations.some(r => r.includes('Critical bug fix'))).toBe(true);
});
it('should provide milestone-specific recommendations', async () => {
const todoContent = `# Project TODO
## Core Features
- [x] Task 1
- [x] Task 2
- [ ] Task 3
- [ ] Task 4
- [ ] Task 5
## Testing
- [x] Task 1
- [ ] Task 2
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
minCompletionRate: 0.8,
});
expect(result.recommendations).toContain('📈 Focus on completing these milestones:');
expect(result.recommendations.some(r => r.includes('Core Features'))).toBe(true);
expect(result.recommendations.some(r => r.includes('Testing'))).toBe(true);
});
});
describe('Release Types', () => {
it('should handle different release types appropriately', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
- [ ] Task 3 (minor enhancement)
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
// Patch release should be more lenient
const patchResult = await analyzeReleaseReadiness({
projectPath: testDir,
releaseType: 'patch',
minCompletionRate: 0.6,
});
// Major release should be more strict
const majorResult = await analyzeReleaseReadiness({
projectPath: testDir,
releaseType: 'major',
minCompletionRate: 0.9,
});
expect(patchResult.isReady).toBe(true);
expect(majorResult.isReady).toBe(false);
});
});
describe('Integration with MCP Tools', () => {
it('should integrate with manage_todo tool', async () => {
// Mock the manage_todo tool - currently disabled in implementation
// const mockManageTodo = jest.fn().mockResolvedValue({
// status: 'success',
// data: {
// completionRate: 0.85,
// criticalTodos: 1
// }
// });
// jest.doMock('../../src/tools/manage-todo-tool.js', () => ({
// manageTodo: mockManageTodo
// }));
const { integrateWithMcpTools } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await integrateWithMcpTools(testDir);
// expect(mockManageTodo).toHaveBeenCalledWith({
// action: 'analyze_progress',
// projectPath: testDir
// });
expect(result).toBeDefined();
});
it('should fallback gracefully when MCP tools fail', async () => {
// Mock the manage_todo tool to fail - currently disabled in implementation
// jest.doMock('../../src/tools/manage-todo-tool.js', () => ({
// manageTodo: jest.fn().mockRejectedValue(new Error('Tool not available'))
// }));
const { integrateWithMcpTools } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await integrateWithMcpTools(testDir);
// Should still work with fallback analysis
expect(result).toBeDefined();
expect(result.score).toBeGreaterThanOrEqual(0);
});
});
describe('Edge Cases and Error Handling', () => {
it('should handle malformed TODO.md gracefully', async () => {
const malformedContent = `# Project TODO
## Feature A
- [x] Task 1
- [INVALID] Task 2
- Task 3 without checkbox
- [x Task 4 missing bracket
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(malformedContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
// Should parse what it can and continue
expect(result.milestones.length).toBeGreaterThan(0);
expect(result.score).toBeGreaterThanOrEqual(0);
});
it('should handle very large TODO.md files', async () => {
// Create a large TODO.md content
let largeContent = '# Project TODO\n\n';
for (let i = 0; i < 100; i++) {
largeContent += `## Feature ${i}\n`;
for (let j = 0; j < 50; j++) {
largeContent += `- [${j % 2 === 0 ? 'x' : ' '}] Task ${j}\n`;
}
}
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(largeContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
expect(result.milestones).toHaveLength(100);
expect(result.score).toBeGreaterThanOrEqual(0);
});
it('should handle file system errors gracefully', async () => {
mockExistsSync.mockImplementation(() => {
throw new Error('File system error');
});
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
// Should have error blocker but not completely fail
expect(result.blockers.some(b => b.type === 'unstable-code')).toBe(true);
expect(result.score).toBeGreaterThanOrEqual(0);
});
});
describe('Summary Generation', () => {
it('should generate comprehensive summaries', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
- [ ] Task 3 (critical)
## Feature B
- [x] Task 1
- [x] Task 2
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
expect(result.summary).toContain('Release Readiness:');
expect(result.summary).toContain('**Score**:');
expect(result.summary).toContain('Milestone Status');
expect(result.summary).toContain('Feature A');
expect(result.summary).toContain('Feature B');
});
it('should format percentages correctly', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
- [x] Task 3
- [ ] Task 4
- [ ] Task 5
- [ ] Task 6
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
// 3/6 = 50.0%
expect(result.summary).toContain('50.0%');
});
});
describe('Performance Tests', () => {
it('should complete analysis within reasonable time', async () => {
const todoContent = `# Project TODO
## Feature A
- [x] Task 1
- [x] Task 2
- [ ] Task 3
`;
mockExistsSync.mockReturnValue(true);
mockReadFileSync.mockReturnValue(todoContent);
mockExecSync.mockReturnValue('');
const { analyzeReleaseReadiness } = await import(
'../../src/utils/release-readiness-detector.js'
);
const startTime = Date.now();
const result = await analyzeReleaseReadiness({
projectPath: testDir,
});
const duration = Date.now() - startTime;
expect(duration).toBeLessThan(1000); // Should complete within 1 second
expect(result).toBeDefined();
});
});
});