Skip to main content
Glama
quality-metrics.aggregate.test.ts29.5 kB
/** * @fileoverview Tests for QualityMetrics aggregate */ import { describe, it, expect, beforeEach } from 'vitest'; import { QualityMetrics } from '../quality-metrics.aggregate.js'; import type { CreateQualityMetricsParams, MetricConfiguration, UpdateMetricConfigParams, RecordMeasurementParams, MetricHistoryEntry, } from '../quality-metrics.types.js'; import type { ProjectKey, GraphQLNodeId } from '../../../../types/branded.js'; import { MetricShortcode, MetricKey } from '../../../../models/metrics.js'; import { ThresholdValue } from '../../../value-objects/threshold-value.js'; import { MetricValue } from '../../../value-objects/metric-value.js'; describe('QualityMetrics Aggregate', () => { let validParams: CreateQualityMetricsParams; let projectKey: ProjectKey; let repositoryId: GraphQLNodeId; let configuration: MetricConfiguration; beforeEach(() => { projectKey = 'test-project' as ProjectKey; repositoryId = 'repo-456' as GraphQLNodeId; configuration = { name: 'Line Coverage', description: 'Percentage of lines covered by tests', shortcode: MetricShortcode.LCV, metricKey: MetricKey.AGGREGATE, unit: '%', minAllowed: 0, maxAllowed: 100, positiveDirection: 'UPWARD', isReported: true, isThresholdEnforced: true, threshold: ThresholdValue.createPercentage(80), }; validParams = { projectKey, repositoryId, configuration, }; }); describe('create', () => { it('should create quality metrics with valid parameters', () => { const metrics = QualityMetrics.create(validParams); expect(metrics.projectKey).toBe(projectKey); expect(metrics.repositoryId).toBe(repositoryId); expect(metrics.configuration).toEqual(configuration); expect(metrics.currentValue).toBeNull(); expect(metrics.history).toHaveLength(0); expect(metrics.domainEvents).toHaveLength(1); expect(metrics.domainEvents[0].eventType).toBe('QualityMetricsCreated'); }); it('should create metrics with current value', () => { const currentValue = MetricValue.create(85.5, '%'); const params = { ...validParams, currentValue, }; const metrics = QualityMetrics.create(params); expect(metrics.currentValue).toEqual(currentValue); }); it('should create metrics with history', () => { const history: MetricHistoryEntry[] = [ { value: MetricValue.create(75, '%'), threshold: ThresholdValue.createPercentage(80), thresholdStatus: 'FAILING', commitOid: 'commit1', recordedAt: new Date('2024-01-01'), }, ]; const params = { ...validParams, history, }; const metrics = QualityMetrics.create(params); expect(metrics.history).toHaveLength(1); expect(metrics.history[0]).toEqual(history[0]); }); it('should create composite ID from project key, metric key, and shortcode', () => { const metrics = QualityMetrics.create(validParams); // The ID should be a composite of projectKey:metricKey:shortcode expect(metrics.id).toBe(`${projectKey}:${MetricKey.AGGREGATE}:${MetricShortcode.LCV}`); }); it('should throw error for empty metric name', () => { const params = { ...validParams, configuration: { ...configuration, name: '', }, }; expect(() => QualityMetrics.create(params)).toThrow('Metric name cannot be empty'); }); it('should throw error for whitespace-only metric name', () => { const params = { ...validParams, configuration: { ...configuration, name: ' ', }, }; expect(() => QualityMetrics.create(params)).toThrow('Metric name cannot be empty'); }); it('should throw error when min allowed >= max allowed', () => { const params = { ...validParams, configuration: { ...configuration, minAllowed: 100, maxAllowed: 100, }, }; expect(() => QualityMetrics.create(params)).toThrow( 'Min allowed value must be less than max allowed value' ); }); it('should create metrics without threshold', () => { const params = { ...validParams, configuration: { ...configuration, threshold: null, }, }; const metrics = QualityMetrics.create(params); expect(metrics.configuration.threshold).toBeNull(); }); it('should emit QualityMetricsCreated event with correct payload', () => { const metrics = QualityMetrics.create(validParams); const events = metrics.domainEvents; expect(events).toHaveLength(1); const event = events[0]; expect(event.eventType).toBe('QualityMetricsCreated'); expect(event.payload).toEqual({ projectKey, metricKey: MetricKey.AGGREGATE, shortcode: MetricShortcode.LCV, threshold: 80, }); }); }); describe('fromPersistence', () => { it('should recreate metrics from persistence without events', () => { const currentValue = MetricValue.create(85.5, '%'); const history: MetricHistoryEntry[] = [ { value: MetricValue.create(80, '%'), threshold: ThresholdValue.createPercentage(80), thresholdStatus: 'PASSING', commitOid: 'commit1', recordedAt: new Date('2024-01-01'), }, ]; const persistenceData = { id: `${projectKey}:${MetricKey.AGGREGATE}:${MetricShortcode.LCV}`, projectKey, repositoryId, configuration, currentValue, history, lastUpdated: new Date('2024-01-02'), }; const metrics = QualityMetrics.fromPersistence(persistenceData); expect(metrics.projectKey).toBe(projectKey); expect(metrics.currentValue).toEqual(currentValue); expect(metrics.history).toEqual(history); expect(metrics.domainEvents).toHaveLength(0); // No events when loading expect(metrics.lastUpdated).toEqual(persistenceData.lastUpdated); }); }); describe('threshold management', () => { describe('updateThreshold', () => { it('should update threshold to new value', () => { const metrics = QualityMetrics.create(validParams); metrics.clearEvents(); const newThreshold = ThresholdValue.createPercentage(90); metrics.updateThreshold(newThreshold); expect(metrics.configuration.threshold).toEqual(newThreshold); expect(metrics.domainEvents).toHaveLength(2); // MetricThresholdUpdated + AggregateModified expect(metrics.domainEvents[0].eventType).toBe('MetricThresholdUpdated'); expect(metrics.domainEvents[0].payload).toEqual({ oldThreshold: 80, newThreshold: 90, unit: '%', }); }); it('should allow removing threshold', () => { const metrics = QualityMetrics.create(validParams); metrics.clearEvents(); metrics.updateThreshold(null); expect(metrics.configuration.threshold).toBeNull(); expect(metrics.domainEvents[0].payload).toEqual({ oldThreshold: 80, newThreshold: undefined, unit: '%', }); }); it('should throw error for mismatched units', () => { const metrics = QualityMetrics.create(validParams); const wrongThreshold = ThresholdValue.create(80, 'ms', 0, 1000); expect(() => metrics.updateThreshold(wrongThreshold)).toThrow( "Threshold unit 'ms' does not match metric unit '%'" ); }); it('should throw error for threshold outside allowed range', () => { const metrics = QualityMetrics.create(validParams); const outOfRangeThreshold = ThresholdValue.create(150, '%', 0, 200); expect(() => metrics.updateThreshold(outOfRangeThreshold)).toThrow( 'Threshold value is outside allowed range for this metric' ); }); it('should update lastUpdated timestamp', async () => { const metrics = QualityMetrics.create(validParams); const originalLastUpdated = metrics.lastUpdated; // Wait a bit to ensure timestamp difference await new Promise((resolve) => setTimeout(resolve, 10)); metrics.updateThreshold(ThresholdValue.createPercentage(85)); expect(metrics.lastUpdated.getTime()).toBeGreaterThan(originalLastUpdated.getTime()); }); }); }); describe('configuration management', () => { describe('updateConfiguration', () => { it('should update metric name', () => { const metrics = QualityMetrics.create(validParams); metrics.clearEvents(); const params: UpdateMetricConfigParams = { name: 'Updated Line Coverage', }; metrics.updateConfiguration(params); expect(metrics.configuration.name).toBe('Updated Line Coverage'); expect(metrics.domainEvents[0].eventType).toBe('MetricConfigurationUpdated'); expect(metrics.domainEvents[0].payload).toEqual(params); }); it('should trim metric name', () => { const metrics = QualityMetrics.create(validParams); metrics.updateConfiguration({ name: ' New Name ' }); expect(metrics.configuration.name).toBe('New Name'); }); it('should throw error for empty name update', () => { const metrics = QualityMetrics.create(validParams); expect(() => metrics.updateConfiguration({ name: '' })).toThrow( 'Metric name cannot be empty' ); }); it('should update description', () => { const metrics = QualityMetrics.create(validParams); metrics.updateConfiguration({ description: 'New description' }); expect(metrics.configuration.description).toBe('New description'); }); it('should update isReported flag', () => { const metrics = QualityMetrics.create(validParams); metrics.updateConfiguration({ isReported: false }); expect(metrics.configuration.isReported).toBe(false); }); it('should update isThresholdEnforced flag', () => { const metrics = QualityMetrics.create(validParams); metrics.updateConfiguration({ isThresholdEnforced: false }); expect(metrics.configuration.isThresholdEnforced).toBe(false); }); it('should update multiple fields at once', () => { const metrics = QualityMetrics.create(validParams); metrics.clearEvents(); const params: UpdateMetricConfigParams = { name: 'New Name', description: 'New description', isReported: false, isThresholdEnforced: false, }; metrics.updateConfiguration(params); expect(metrics.configuration.name).toBe('New Name'); expect(metrics.configuration.description).toBe('New description'); expect(metrics.configuration.isReported).toBe(false); expect(metrics.configuration.isThresholdEnforced).toBe(false); expect(metrics.domainEvents).toHaveLength(2); }); it('should not emit event when no changes made', () => { const metrics = QualityMetrics.create(validParams); metrics.clearEvents(); // Update with same values metrics.updateConfiguration({ name: configuration.name, isReported: configuration.isReported, }); expect(metrics.domainEvents).toHaveLength(0); }); it('should update lastUpdated timestamp on change', async () => { const metrics = QualityMetrics.create(validParams); const originalLastUpdated = metrics.lastUpdated; await new Promise((resolve) => setTimeout(resolve, 10)); metrics.updateConfiguration({ name: 'New Name' }); expect(metrics.lastUpdated.getTime()).toBeGreaterThan(originalLastUpdated.getTime()); }); }); }); describe('measurement recording', () => { describe('recordMeasurement', () => { it('should record a new measurement', () => { const metrics = QualityMetrics.create(validParams); metrics.clearEvents(); const params: RecordMeasurementParams = { value: 85.5, commitOid: 'abc123', }; metrics.recordMeasurement(params); expect(metrics.currentValue).not.toBeNull(); expect(metrics.currentValue?.value).toBe(85.5); expect(metrics.currentValue?.unit).toBe('%'); expect(metrics.history).toHaveLength(1); expect(metrics.history[0].commitOid).toBe('abc123'); expect(metrics.domainEvents[0].eventType).toBe('MeasurementRecorded'); }); it('should use provided timestamp', () => { const metrics = QualityMetrics.create(validParams); const customDate = new Date('2024-01-15'); metrics.recordMeasurement({ value: 85.5, commitOid: 'abc123', measuredAt: customDate, }); expect(metrics.history[0].recordedAt).toEqual(customDate); expect(metrics.currentValue?.measuredAt).toEqual(customDate); }); it('should throw error for value outside allowed range', () => { const metrics = QualityMetrics.create(validParams); expect(() => metrics.recordMeasurement({ value: 150, commitOid: 'abc123', }) ).toThrow('Value 150 is outside allowed range [0, 100]'); expect(() => metrics.recordMeasurement({ value: -10, commitOid: 'abc123', }) ).toThrow('Value -10 is outside allowed range [0, 100]'); }); it('should update threshold status in history', () => { const metrics = QualityMetrics.create(validParams); // Record passing value metrics.recordMeasurement({ value: 85, commitOid: 'commit1', }); expect(metrics.history[0].thresholdStatus).toBe('PASSING'); // Record failing value metrics.recordMeasurement({ value: 75, commitOid: 'commit2', }); expect(metrics.history[1].thresholdStatus).toBe('FAILING'); }); it('should handle metrics without threshold', () => { const params = { ...validParams, configuration: { ...configuration, threshold: null, }, }; const metrics = QualityMetrics.create(params); metrics.recordMeasurement({ value: 85, commitOid: 'commit1', }); expect(metrics.history[0].thresholdStatus).toBe('UNKNOWN'); }); it('should trim history to max entries', () => { const metrics = QualityMetrics.create(validParams); // Record more than MAX_HISTORY_ENTRIES (100) for (let i = 0; i < 105; i++) { metrics.recordMeasurement({ value: 80 + (i % 20), commitOid: `commit${i}`, }); } expect(metrics.history).toHaveLength(100); // First entries should be removed expect(metrics.history[0].commitOid).toBe('commit5'); expect(metrics.history[99].commitOid).toBe('commit104'); }); it('should emit MeasurementRecorded event with correct payload', () => { const metrics = QualityMetrics.create(validParams); metrics.clearEvents(); metrics.recordMeasurement({ value: 85.5, commitOid: 'abc123', }); const event = metrics.domainEvents[0]; expect(event.eventType).toBe('MeasurementRecorded'); expect(event.payload).toEqual({ value: 85.5, unit: '%', commitOid: 'abc123', thresholdStatus: 'PASSING', }); }); }); }); describe('compliance evaluation', () => { it('should evaluate as compliant when value meets upward threshold', () => { const metrics = QualityMetrics.create(validParams); metrics.recordMeasurement({ value: 85, commitOid: 'commit1', }); expect(metrics.isCompliant).toBe(true); expect(metrics.thresholdStatus).toBe('PASSING'); }); it('should evaluate as non-compliant when value fails upward threshold', () => { const metrics = QualityMetrics.create(validParams); metrics.recordMeasurement({ value: 75, commitOid: 'commit1', }); expect(metrics.isCompliant).toBe(false); expect(metrics.thresholdStatus).toBe('FAILING'); }); it('should evaluate downward metrics correctly', () => { const params = { ...validParams, configuration: { ...configuration, shortcode: MetricShortcode.DDP, // Duplicate code percentage positiveDirection: 'DOWNWARD' as const, threshold: ThresholdValue.createPercentage(10), // Max 10% duplication }, }; const metrics = QualityMetrics.create(params); // 5% duplication - should pass metrics.recordMeasurement({ value: 5, commitOid: 'commit1', }); expect(metrics.isCompliant).toBe(true); // 15% duplication - should fail metrics.recordMeasurement({ value: 15, commitOid: 'commit2', }); expect(metrics.isCompliant).toBe(false); }); it('should be compliant when no threshold set', () => { const params = { ...validParams, configuration: { ...configuration, threshold: null, }, }; const metrics = QualityMetrics.create(params); metrics.recordMeasurement({ value: 50, commitOid: 'commit1', }); expect(metrics.isCompliant).toBe(true); expect(metrics.thresholdStatus).toBe('UNKNOWN'); }); it('should be compliant when no value recorded', () => { const metrics = QualityMetrics.create(validParams); expect(metrics.isCompliant).toBe(true); expect(metrics.thresholdStatus).toBe('UNKNOWN'); }); describe('evaluateCompliance', () => { it('should evaluate arbitrary values', () => { const metrics = QualityMetrics.create(validParams); expect(metrics.evaluateCompliance(90)).toBe(true); expect(metrics.evaluateCompliance(70)).toBe(false); expect(metrics.evaluateCompliance(80)).toBe(true); // Exactly at threshold }); it('should return true when no threshold', () => { const params = { ...validParams, configuration: { ...configuration, threshold: null, }, }; const metrics = QualityMetrics.create(params); expect(metrics.evaluateCompliance(50)).toBe(true); expect(metrics.evaluateCompliance(0)).toBe(true); }); }); }); describe('trend analysis', () => { describe('getTrend', () => { it('should return null with insufficient data', () => { const metrics = QualityMetrics.create(validParams); expect(metrics.getTrend()).toBeNull(); metrics.recordMeasurement({ value: 80, commitOid: 'commit1', }); expect(metrics.getTrend()).toBeNull(); // Still only 1 entry }); it('should calculate improving trend for upward metrics', () => { const metrics = QualityMetrics.create(validParams); const now = new Date(); const daysAgo = (days: number) => { const date = new Date(now); date.setDate(date.getDate() - days); return date; }; // Record measurements over time metrics.recordMeasurement({ value: 70, commitOid: 'commit1', measuredAt: daysAgo(20), }); metrics.recordMeasurement({ value: 75, commitOid: 'commit2', measuredAt: daysAgo(10), }); metrics.recordMeasurement({ value: 80, commitOid: 'commit3', measuredAt: daysAgo(5), }); metrics.recordMeasurement({ value: 85, commitOid: 'commit4', measuredAt: now, }); const trend = metrics.getTrend(30); expect(trend).not.toBeNull(); expect(trend?.direction).toBe('IMPROVING'); expect(trend?.changePercentage).toBeCloseTo(21.43, 1); // (85-70)/70 * 100 expect(trend?.periodDays).toBe(30); }); it('should calculate degrading trend for upward metrics', () => { const metrics = QualityMetrics.create(validParams); const now = new Date(); const daysAgo = (days: number) => { const date = new Date(now); date.setDate(date.getDate() - days); return date; }; metrics.recordMeasurement({ value: 90, commitOid: 'commit1', measuredAt: daysAgo(20), }); metrics.recordMeasurement({ value: 80, commitOid: 'commit2', measuredAt: now, }); const trend = metrics.getTrend(30); expect(trend?.direction).toBe('DEGRADING'); expect(trend?.changePercentage).toBeCloseTo(-11.11, 1); }); it('should calculate stable trend for minimal changes', () => { const metrics = QualityMetrics.create(validParams); const now = new Date(); const daysAgo = (days: number) => { const date = new Date(now); date.setDate(date.getDate() - days); return date; }; metrics.recordMeasurement({ value: 80, commitOid: 'commit1', measuredAt: daysAgo(20), }); metrics.recordMeasurement({ value: 80.5, commitOid: 'commit2', measuredAt: now, }); const trend = metrics.getTrend(30); expect(trend?.direction).toBe('STABLE'); // Less than 1% change }); it('should handle downward metrics correctly', () => { const params = { ...validParams, configuration: { ...configuration, positiveDirection: 'DOWNWARD' as const, }, }; const metrics = QualityMetrics.create(params); const now = new Date(); const daysAgo = (days: number) => { const date = new Date(now); date.setDate(date.getDate() - days); return date; }; // For downward metrics, decreasing values are improving metrics.recordMeasurement({ value: 20, commitOid: 'commit1', measuredAt: daysAgo(20), }); metrics.recordMeasurement({ value: 15, commitOid: 'commit2', measuredAt: now, }); const trend = metrics.getTrend(30); expect(trend?.direction).toBe('IMPROVING'); // Decreasing is good for downward metrics }); it('should respect period parameter', () => { const metrics = QualityMetrics.create(validParams); const now = new Date(); const daysAgo = (days: number) => { const date = new Date(now); date.setDate(date.getDate() - days); return date; }; // Old measurement outside period metrics.recordMeasurement({ value: 60, commitOid: 'commit1', measuredAt: daysAgo(40), }); // Recent measurements metrics.recordMeasurement({ value: 80, commitOid: 'commit2', measuredAt: daysAgo(20), }); metrics.recordMeasurement({ value: 85, commitOid: 'commit3', measuredAt: now, }); const trend = metrics.getTrend(30); // Only last 30 days // Should only consider last two measurements expect(trend?.changePercentage).toBeCloseTo(6.25, 1); // (85-80)/80 * 100 }); }); }); describe('getters', () => { it('should return immutable configuration', () => { const metrics = QualityMetrics.create(validParams); const config = metrics.configuration; // Verify it's a copy expect(config).not.toBe(configuration); expect(config).toEqual(configuration); }); it('should return history as array', () => { const metrics = QualityMetrics.create(validParams); metrics.recordMeasurement({ value: 80, commitOid: 'commit1', }); metrics.recordMeasurement({ value: 85, commitOid: 'commit2', }); const history = metrics.history; expect(Array.isArray(history)).toBe(true); expect(history).toHaveLength(2); }); }); describe('toPersistence', () => { it('should convert to persistence format', () => { const metrics = QualityMetrics.create(validParams); metrics.recordMeasurement({ value: 85.5, commitOid: 'abc123', }); const persistence = metrics.toPersistence(); expect(persistence).toEqual({ id: `${projectKey}:${MetricKey.AGGREGATE}:${MetricShortcode.LCV}`, projectKey, repositoryId, configuration, currentValue: expect.objectContaining({ value: 85.5, unit: '%', }), history: expect.arrayContaining([ expect.objectContaining({ value: expect.objectContaining({ value: 85.5 }), commitOid: 'abc123', }), ]), lastUpdated: expect.any(Date), }); }); it('should preserve all data through persistence round-trip', () => { const metrics = QualityMetrics.create(validParams); metrics.recordMeasurement({ value: 75, commitOid: 'commit1', }); metrics.updateThreshold(ThresholdValue.createPercentage(70)); metrics.recordMeasurement({ value: 85, commitOid: 'commit2', }); const persistence = metrics.toPersistence(); const reconstructed = QualityMetrics.fromPersistence(persistence); expect(reconstructed.projectKey).toBe(metrics.projectKey); expect(reconstructed.configuration).toEqual(metrics.configuration); expect(reconstructed.currentValue?.value).toBe(85); expect(reconstructed.history).toHaveLength(2); expect(reconstructed.lastUpdated).toEqual(metrics.lastUpdated); }); }); describe('domain events', () => { it('should accumulate multiple events', () => { const metrics = QualityMetrics.create(validParams); metrics.clearEvents(); metrics.updateThreshold(ThresholdValue.createPercentage(90)); metrics.recordMeasurement({ value: 95, commitOid: 'commit1', }); metrics.updateConfiguration({ name: 'New Name' }); // Each operation emits 2 events (operation + AggregateModified) expect(metrics.domainEvents).toHaveLength(6); const eventTypes = metrics.domainEvents.map((e) => e.eventType); expect(eventTypes).toEqual([ 'MetricThresholdUpdated', 'AggregateModified', 'MeasurementRecorded', 'AggregateModified', 'MetricConfigurationUpdated', 'AggregateModified', ]); }); }); describe('edge cases', () => { it('should handle all metric shortcodes', () => { const shortcodes = [ MetricShortcode.LCV, MetricShortcode.BCV, MetricShortcode.DCV, MetricShortcode.DDP, MetricShortcode.SCV, MetricShortcode.TCV, MetricShortcode.CMP, ]; shortcodes.forEach((shortcode) => { const params = { ...validParams, configuration: { ...configuration, shortcode, }, }; const metrics = QualityMetrics.create(params); expect(metrics.configuration.shortcode).toBe(shortcode); }); }); it('should handle all metric keys', () => { const metricKeys = [ MetricKey.AGGREGATE, MetricKey.PYTHON, MetricKey.JAVASCRIPT, MetricKey.TYPESCRIPT, MetricKey.GO, MetricKey.JAVA, MetricKey.RUBY, MetricKey.RUST, ]; metricKeys.forEach((metricKey) => { const params = { ...validParams, configuration: { ...configuration, metricKey, }, }; const metrics = QualityMetrics.create(params); expect(metrics.configuration.metricKey).toBe(metricKey); }); }); it('should handle negative value ranges', () => { const params = { ...validParams, configuration: { ...configuration, minAllowed: -100, maxAllowed: 100, threshold: ThresholdValue.create(0, 'delta', -100, 100), }, }; const metrics = QualityMetrics.create(params); metrics.recordMeasurement({ value: -50, commitOid: 'commit1', }); expect(metrics.currentValue?.value).toBe(-50); }); it('should handle exact threshold values', () => { const metrics = QualityMetrics.create(validParams); // Record value exactly at threshold metrics.recordMeasurement({ value: 80, commitOid: 'commit1', }); expect(metrics.isCompliant).toBe(true); // Threshold is inclusive expect(metrics.thresholdStatus).toBe('PASSING'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sapientpants/deepsource-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server