mirror of
https://github.com/johndoe6345789/snippet-pastebin.git
synced 2026-05-06 03:09:34 +00:00
0c3293acc8
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>
297 lines
8.6 KiB
TypeScript
297 lines
8.6 KiB
TypeScript
/**
|
|
* Unit Tests for Architecture Checker
|
|
* Tests component validation, dependency analysis, and pattern compliance
|
|
*/
|
|
|
|
import { ArchitectureChecker } from '../../../src/lib/quality-validator/analyzers/architectureChecker';
|
|
import { logger } from '../../../src/lib/quality-validator/utils/logger';
|
|
import { createTempDir, cleanupTempDir, createTestFile } from '../../test-utils';
|
|
|
|
describe('ArchitectureChecker', () => {
|
|
let checker: ArchitectureChecker;
|
|
let tempDir: string;
|
|
|
|
beforeEach(() => {
|
|
checker = new ArchitectureChecker();
|
|
tempDir = createTempDir();
|
|
logger.configure({ verbose: false, useColors: false });
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanupTempDir(tempDir);
|
|
});
|
|
|
|
describe('analyze', () => {
|
|
it('should analyze architecture and return result', async () => {
|
|
const filePath = createTestFile(
|
|
tempDir,
|
|
'src/components/atoms/Button.tsx',
|
|
'export const Button = () => <button>Click</button>;'
|
|
);
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze(['src/components/atoms/Button.tsx']);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.category).toBe('architecture');
|
|
expect(typeof result.score).toBe('number');
|
|
expect(result.status).toMatch(/pass|fail|warning/);
|
|
expect(Array.isArray(result.findings)).toBe(true);
|
|
expect(result.metrics).toBeDefined();
|
|
expect(typeof result.executionTime).toBe('number');
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
it('should handle empty file list', async () => {
|
|
const result = await checker.analyze([]);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.category).toBe('architecture');
|
|
expect(typeof result.score).toBe('number');
|
|
});
|
|
});
|
|
|
|
describe('Component Analysis', () => {
|
|
it('should classify components by folder structure', async () => {
|
|
createTestFile(tempDir, 'src/components/atoms/Button.tsx', '// Atom');
|
|
createTestFile(tempDir, 'src/components/molecules/Card.tsx', '// Molecule');
|
|
createTestFile(tempDir, 'src/components/organisms/Header.tsx', '// Organism');
|
|
createTestFile(tempDir, 'src/components/templates/Layout.tsx', '// Template');
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze([
|
|
'src/components/atoms/Button.tsx',
|
|
'src/components/molecules/Card.tsx',
|
|
'src/components/organisms/Header.tsx',
|
|
'src/components/templates/Layout.tsx',
|
|
]);
|
|
|
|
const metrics = result.metrics as any;
|
|
expect(metrics.components).toBeDefined();
|
|
expect(metrics.components.byType.atoms).toBeGreaterThanOrEqual(0);
|
|
expect(metrics.components.byType.molecules).toBeGreaterThanOrEqual(0);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
it('should detect oversized components', async () => {
|
|
// Create a large component
|
|
let largeComponentCode = '// Large component\n';
|
|
for (let i = 0; i < 600; i++) {
|
|
largeComponentCode += `// Line ${i}\n`;
|
|
}
|
|
|
|
createTestFile(tempDir, 'src/components/organisms/Large.tsx', largeComponentCode);
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze(['src/components/organisms/Large.tsx']);
|
|
|
|
const metrics = result.metrics as any;
|
|
expect(metrics.components).toBeDefined();
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
it('should calculate average component size', async () => {
|
|
createTestFile(tempDir, 'src/components/atoms/A.tsx', '// ' + 'x'.repeat(100));
|
|
createTestFile(tempDir, 'src/components/atoms/B.tsx', '// ' + 'x'.repeat(100));
|
|
createTestFile(tempDir, 'src/components/atoms/C.tsx', '// ' + 'x'.repeat(100));
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze([
|
|
'src/components/atoms/A.tsx',
|
|
'src/components/atoms/B.tsx',
|
|
'src/components/atoms/C.tsx',
|
|
]);
|
|
|
|
const metrics = result.metrics as any;
|
|
expect(metrics.components.averageSize).toBeGreaterThan(0);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Dependency Analysis', () => {
|
|
it('should extract import statements', async () => {
|
|
const filePath = createTestFile(
|
|
tempDir,
|
|
'src/components/Button.tsx',
|
|
`
|
|
import React from 'react';
|
|
import { useState } from 'react';
|
|
import Button from './Button';
|
|
`
|
|
);
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze(['src/components/Button.tsx']);
|
|
|
|
const metrics = result.metrics as any;
|
|
expect(metrics.dependencies).toBeDefined();
|
|
expect(metrics.dependencies.totalModules).toBeGreaterThanOrEqual(0);
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
it('should track external dependencies', async () => {
|
|
createTestFile(
|
|
tempDir,
|
|
'src/app.ts',
|
|
`
|
|
import React from 'react';
|
|
import lodash from 'lodash';
|
|
import { Button } from './components';
|
|
`
|
|
);
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze(['src/app.ts']);
|
|
|
|
const metrics = result.metrics as any;
|
|
expect(metrics.dependencies.externalDependencies).toBeDefined();
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
it('should detect circular dependencies', async () => {
|
|
createTestFile(
|
|
tempDir,
|
|
'src/a.ts',
|
|
"import { B } from './b';\nexport const A = () => B();"
|
|
);
|
|
createTestFile(
|
|
tempDir,
|
|
'src/b.ts',
|
|
"import { A } from './a';\nexport const B = () => A();"
|
|
);
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze(['src/a.ts', 'src/b.ts']);
|
|
|
|
const metrics = result.metrics as any;
|
|
expect(metrics.dependencies.circularDependencies).toBeDefined();
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Pattern Analysis', () => {
|
|
it('should detect Redux mutations', async () => {
|
|
createTestFile(
|
|
tempDir,
|
|
'src/store/slices/counter.ts',
|
|
`
|
|
export const counterSlice = {
|
|
reducer: (state) => {
|
|
state.count = state.count + 1;
|
|
}
|
|
};
|
|
`
|
|
);
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze(['src/store/slices/counter.ts']);
|
|
|
|
const metrics = result.metrics as any;
|
|
expect(metrics.patterns.reduxCompliance).toBeDefined();
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
|
|
it('should detect hooks not at top level', async () => {
|
|
createTestFile(
|
|
tempDir,
|
|
'src/components/BadHook.tsx',
|
|
`
|
|
export function Component() {
|
|
if (condition) {
|
|
const [state, setState] = useState(0);
|
|
}
|
|
}
|
|
`
|
|
);
|
|
|
|
const originalCwd = process.cwd();
|
|
process.chdir(tempDir);
|
|
|
|
try {
|
|
const result = await checker.analyze(['src/components/BadHook.tsx']);
|
|
|
|
const metrics = result.metrics as any;
|
|
expect(metrics.patterns.hookUsage).toBeDefined();
|
|
} finally {
|
|
process.chdir(originalCwd);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Score Calculation', () => {
|
|
it('should return score between 0 and 100', async () => {
|
|
const result = await checker.analyze([]);
|
|
|
|
expect(result.score).toBeGreaterThanOrEqual(0);
|
|
expect(result.score).toBeLessThanOrEqual(100);
|
|
});
|
|
|
|
it('should assign status based on score', async () => {
|
|
const result = await checker.analyze([]);
|
|
|
|
if (result.score >= 80) {
|
|
expect(result.status).toBe('pass');
|
|
} else if (result.score >= 70) {
|
|
expect(result.status).toBe('warning');
|
|
} else {
|
|
expect(result.status).toBe('fail');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle non-existent files gracefully', async () => {
|
|
const result = await checker.analyze(['non-existent.ts']);
|
|
|
|
expect(result).toBeDefined();
|
|
expect(result.category).toBe('architecture');
|
|
});
|
|
|
|
it('should measure execution time', async () => {
|
|
const result = await checker.analyze([]);
|
|
|
|
expect(result.executionTime).toBeGreaterThanOrEqual(0);
|
|
});
|
|
});
|
|
});
|