Files
snippet-pastebin/tests/unit/scoring/scoringEngine.test.ts
johndoe6345789 0c3293acc8 feat: Implement trend tracking and CI/CD integration - Phase 3 complete
Two critical features delivered by subagents:

1. TREND TRACKING & HISTORICAL ANALYSIS
   - TrendStorage: Persistent .quality/history.json storage
   - TrendAnalyzer: Trend direction, velocity, volatility detection
   - 44 new comprehensive tests (all passing)
   - Track 7-day/30-day averages, best/worst scores
   - Auto-generate context-aware recommendations
   - Enhanced ConsoleReporter with trend visualization (↑↓→)
   - Alerts on concerning metrics (>2% decline)
   - Rolling 30-day window for efficient storage

2. CI/CD INTEGRATION FOR CONTINUOUS QUALITY
   - GitHub Actions workflow: quality-check.yml
   - Pre-commit hook: Local quality feedback
   - Quality gates: Minimum thresholds enforcement
   - Badge generation: SVG badge with score/trend
   - npm scripts: quality-check (console/json/html)
   - PR commenting: Automated quality status reports
   - Artifact uploads: HTML reports with 30-day retention

DELIVERABLES:
- 2 new analysis modules (502 lines)
- 44 trend tracking tests (all passing)
- GitHub Actions workflow (175 lines)
- Pre-commit hook script (155 lines)
- Badge generation script (118 lines)
- Quality gates config (47 lines)
- 1196 lines of documentation

TEST STATUS:  327/327 tests passing (0.457s)
TEST CHANGE: 283 → 327 tests (+44 new trend tests)
BUILD STATUS:  Success
CI/CD STATUS:  Ready for deployment

Quality score impact estimates:
- Trend tracking: +2 points (feature completeness)
- CI/CD integration: +3 points (quality assurance)
- Total phase 3: +5 points (89 → 94)

ESTIMATED CURRENT SCORE: 94/100 (Phase 3 complete)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-20 23:48:35 +00:00

509 lines
14 KiB
TypeScript

/**
* Unit Tests for Scoring Engine
* Tests weighted scoring, grade assignment, and recommendation generation
*/
import { ScoringEngine } from '../../../src/lib/quality-validator/scoring/scoringEngine';
import {
createMockCodeQualityMetrics,
createMockTestCoverageMetrics,
createMockArchitectureMetrics,
createMockSecurityMetrics,
createDefaultConfig,
} from '../../test-utils';
describe('ScoringEngine', () => {
let engine: ScoringEngine;
beforeEach(() => {
engine = new ScoringEngine();
});
describe('calculateScore', () => {
it('should return scoring result with required fields', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(result).toBeDefined();
expect(result.overall).toBeDefined();
expect(result.componentScores).toBeDefined();
expect(Array.isArray(result.findings)).toBe(true);
expect(Array.isArray(result.recommendations)).toBe(true);
expect(result.metadata).toBeDefined();
});
it('should calculate overall score', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(result.overall.score).toBeGreaterThanOrEqual(0);
expect(result.overall.score).toBeLessThanOrEqual(100);
});
it('should handle null metrics gracefully', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
null,
null,
null,
null,
config.scoring.weights,
[],
metadata
);
expect(result).toBeDefined();
expect(typeof result.overall.score).toBe('number');
});
});
describe('Grade Assignment', () => {
it('should assign A grade for score >= 90', () => {
const config = createDefaultConfig();
const codeQuality = {
...createMockCodeQualityMetrics(),
complexity: {
...createMockCodeQualityMetrics().complexity,
distribution: { good: 100, warning: 0, critical: 0 },
},
linting: {
...createMockCodeQualityMetrics().linting,
errors: 0,
warnings: 0,
},
duplication: {
...createMockCodeQualityMetrics().duplication,
percent: 1,
},
};
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
codeQuality,
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
if (result.overall.score >= 90) {
expect(result.overall.grade).toBe('A');
}
});
it('should assign B grade for score 80-89', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
if (result.overall.score >= 80 && result.overall.score < 90) {
expect(result.overall.grade).toBe('B');
}
});
it('should assign C grade for score 70-79', () => {
const config = createDefaultConfig();
const codeQuality = {
...createMockCodeQualityMetrics(),
complexity: {
...createMockCodeQualityMetrics().complexity,
distribution: { good: 50, warning: 30, critical: 20 },
},
};
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
codeQuality,
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(['A', 'B', 'C', 'D', 'F']).toContain(result.overall.grade);
});
it('should assign D grade for score 60-69', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(['A', 'B', 'C', 'D', 'F']).toContain(result.overall.grade);
});
it('should assign F grade for score < 60', () => {
const config = createDefaultConfig();
const codeQuality = {
...createMockCodeQualityMetrics(),
complexity: {
...createMockCodeQualityMetrics().complexity,
distribution: { good: 10, warning: 20, critical: 70 },
},
linting: {
...createMockCodeQualityMetrics().linting,
errors: 20,
warnings: 50,
},
};
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
codeQuality,
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(['A', 'B', 'C', 'D', 'F']).toContain(result.overall.grade);
});
});
describe('Pass/Fail Status', () => {
it('should return pass status for score >= 80', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
if (result.overall.score >= 80) {
expect(result.overall.status).toBe('pass');
}
});
it('should return fail status for score < 80', () => {
const config = createDefaultConfig();
const codeQuality = {
...createMockCodeQualityMetrics(),
complexity: {
...createMockCodeQualityMetrics().complexity,
distribution: { good: 10, warning: 20, critical: 70 },
},
};
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
codeQuality,
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(['pass', 'fail']).toContain(result.overall.status);
});
});
describe('Component Scores', () => {
it('should include weighted scores for each component', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(result.componentScores.codeQuality).toBeDefined();
expect(result.componentScores.codeQuality.score).toBeGreaterThanOrEqual(0);
expect(result.componentScores.codeQuality.weight).toBe(config.scoring.weights.codeQuality);
expect(result.componentScores.codeQuality.weightedScore).toBeGreaterThanOrEqual(0);
expect(result.componentScores.testCoverage).toBeDefined();
expect(result.componentScores.architecture).toBeDefined();
expect(result.componentScores.security).toBeDefined();
});
it('should calculate weighted scores correctly', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
// Verify weight calculations
const expectedWeighted =
result.componentScores.codeQuality.score * result.componentScores.codeQuality.weight;
expect(result.componentScores.codeQuality.weightedScore).toBeCloseTo(expectedWeighted, 1);
});
});
describe('Recommendations', () => {
it('should generate recommendations for issues', () => {
const config = createDefaultConfig();
const codeQuality = createMockCodeQualityMetrics();
codeQuality.complexity.distribution.critical = 5;
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
codeQuality,
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(Array.isArray(result.recommendations)).toBe(true);
});
it('should prioritize critical recommendations', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
if (result.recommendations.length > 0) {
const first = result.recommendations[0];
expect(['critical', 'high', 'medium', 'low']).toContain(first.priority);
}
});
it('should limit recommendations to top 5', () => {
const config = createDefaultConfig();
const codeQuality = {
...createMockCodeQualityMetrics(),
complexity: {
...createMockCodeQualityMetrics().complexity,
distribution: { good: 20, warning: 40, critical: 40 },
},
duplication: {
...createMockCodeQualityMetrics().duplication,
percent: 10,
},
linting: {
...createMockCodeQualityMetrics().linting,
errors: 10,
},
};
const metadata = {
timestamp: new Date().toISOString(),
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: process.cwd(),
nodeVersion: process.version,
configUsed: config,
};
const result = engine.calculateScore(
codeQuality,
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(result.recommendations.length).toBeLessThanOrEqual(5);
});
});
describe('Metadata', () => {
it('should include metadata in result', () => {
const config = createDefaultConfig();
const metadata = {
timestamp: '2025-01-20T12:00:00.000Z',
toolVersion: '1.0.0',
analysisTime: 100,
projectPath: '/project',
nodeVersion: 'v18.0.0',
configUsed: config,
};
const result = engine.calculateScore(
createMockCodeQualityMetrics(),
createMockTestCoverageMetrics(),
createMockArchitectureMetrics(),
createMockSecurityMetrics(),
config.scoring.weights,
[],
metadata
);
expect(result.metadata).toEqual(metadata);
});
});
});