services-as-software 2.1.1 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,478 @@
1
+ /**
2
+ * Service Metrics Tests
3
+ *
4
+ * Tests for quality assurance and performance metrics tracking.
5
+ */
6
+
7
+ import { describe, it, expect, vi } from 'vitest'
8
+ import {
9
+ createMetricsTracker,
10
+ trackQuality,
11
+ trackPerformance,
12
+ aggregateMetrics,
13
+ createQualityGate,
14
+ evaluateQuality,
15
+ MetricType,
16
+ QualityDimension,
17
+ } from '../src/metrics/index.js'
18
+
19
+ describe('Service Metrics', () => {
20
+ describe('Metrics Tracker', () => {
21
+ it('should create a metrics tracker', () => {
22
+ const tracker = createMetricsTracker({
23
+ serviceId: 'translation-service',
24
+ retention: '30d',
25
+ })
26
+
27
+ expect(tracker.serviceId).toBe('translation-service')
28
+ expect(tracker.retention).toBe('30d')
29
+ })
30
+
31
+ it('should record metrics', async () => {
32
+ const tracker = createMetricsTracker({
33
+ serviceId: 'test-service',
34
+ })
35
+
36
+ await tracker.record({
37
+ name: 'requests',
38
+ value: 1,
39
+ type: 'counter',
40
+ timestamp: new Date(),
41
+ })
42
+
43
+ const metrics = await tracker.query({ name: 'requests' })
44
+ expect(metrics).toHaveLength(1)
45
+ expect(metrics[0].value).toBe(1)
46
+ })
47
+
48
+ it('should support metric dimensions/labels', async () => {
49
+ const tracker = createMetricsTracker({
50
+ serviceId: 'test-service',
51
+ })
52
+
53
+ await tracker.record({
54
+ name: 'response_time',
55
+ value: 150,
56
+ type: 'gauge',
57
+ dimensions: {
58
+ endpoint: '/translate',
59
+ region: 'us-east',
60
+ },
61
+ })
62
+
63
+ const metrics = await tracker.query({
64
+ name: 'response_time',
65
+ dimensions: { endpoint: '/translate' },
66
+ })
67
+
68
+ expect(metrics).toHaveLength(1)
69
+ expect(metrics[0].dimensions.endpoint).toBe('/translate')
70
+ })
71
+
72
+ it('should aggregate metrics over time', async () => {
73
+ const tracker = createMetricsTracker({
74
+ serviceId: 'test-service',
75
+ })
76
+
77
+ // Record multiple values
78
+ for (let i = 0; i < 10; i++) {
79
+ await tracker.record({
80
+ name: 'latency',
81
+ value: 100 + i * 10, // 100-190
82
+ type: 'histogram',
83
+ })
84
+ }
85
+
86
+ const aggregated = await tracker.aggregate({
87
+ name: 'latency',
88
+ aggregations: ['avg', 'p50', 'p95', 'p99', 'min', 'max'],
89
+ })
90
+
91
+ expect(aggregated.avg).toBe(145)
92
+ expect(aggregated.min).toBe(100)
93
+ expect(aggregated.max).toBe(190)
94
+ })
95
+ })
96
+
97
+ describe('Quality Metrics', () => {
98
+ it('should track quality score', async () => {
99
+ const result = await trackQuality({
100
+ executionId: 'exec-123',
101
+ dimensions: {
102
+ accuracy: 0.95,
103
+ completeness: 0.90,
104
+ timeliness: 1.0,
105
+ customerSatisfaction: 0.85,
106
+ },
107
+ weights: {
108
+ accuracy: 0.3,
109
+ completeness: 0.25,
110
+ timeliness: 0.25,
111
+ customerSatisfaction: 0.2,
112
+ },
113
+ })
114
+
115
+ expect(result.overallScore).toBeCloseTo(0.9225)
116
+ expect(result.dimensions.accuracy).toBe(0.95)
117
+ })
118
+
119
+ it('should track execution quality with AI assessment', async () => {
120
+ const result = await trackQuality({
121
+ executionId: 'exec-456',
122
+ output: 'Generated content here...',
123
+ aiAssessment: {
124
+ enabled: true,
125
+ criteria: ['relevance', 'clarity', 'accuracy'],
126
+ },
127
+ })
128
+
129
+ expect(result.aiScores).toBeDefined()
130
+ expect(result.aiScores.relevance).toBeDefined()
131
+ })
132
+
133
+ it('should compare against baseline quality', async () => {
134
+ const result = await trackQuality({
135
+ executionId: 'exec-789',
136
+ dimensions: { accuracy: 0.85 },
137
+ baseline: { accuracy: 0.80 },
138
+ })
139
+
140
+ expect(result.comparison.accuracy.delta).toBe(0.05)
141
+ expect(result.comparison.accuracy.improved).toBe(true)
142
+ })
143
+
144
+ it('should flag quality issues', async () => {
145
+ const result = await trackQuality({
146
+ executionId: 'exec-111',
147
+ dimensions: {
148
+ accuracy: 0.60, // below threshold
149
+ completeness: 0.95,
150
+ },
151
+ thresholds: {
152
+ accuracy: 0.80,
153
+ completeness: 0.90,
154
+ },
155
+ })
156
+
157
+ expect(result.issues).toContainEqual(
158
+ expect.objectContaining({
159
+ dimension: 'accuracy',
160
+ severity: 'high',
161
+ })
162
+ )
163
+ })
164
+ })
165
+
166
+ describe('Performance Metrics', () => {
167
+ it('should track execution performance', async () => {
168
+ const result = await trackPerformance({
169
+ executionId: 'exec-123',
170
+ metrics: {
171
+ duration: 5000, // ms
172
+ tokensUsed: 1500,
173
+ apiCalls: 3,
174
+ cost: 0.05,
175
+ },
176
+ })
177
+
178
+ expect(result.duration).toBe(5000)
179
+ expect(result.tokensUsed).toBe(1500)
180
+ expect(result.cost).toBe(0.05)
181
+ })
182
+
183
+ it('should track resource utilization', async () => {
184
+ const result = await trackPerformance({
185
+ executionId: 'exec-456',
186
+ resourceUsage: {
187
+ cpu: 45, // percentage
188
+ memory: 512, // MB
189
+ storage: 100, // MB
190
+ },
191
+ })
192
+
193
+ expect(result.resourceUsage.cpu).toBe(45)
194
+ expect(result.resourceUsage.memory).toBe(512)
195
+ })
196
+
197
+ it('should track throughput', async () => {
198
+ const result = await trackPerformance({
199
+ executionId: 'exec-789',
200
+ throughput: {
201
+ requestsPerSecond: 100,
202
+ itemsProcessed: 1000,
203
+ processingRate: 200, // items per minute
204
+ },
205
+ })
206
+
207
+ expect(result.throughput.requestsPerSecond).toBe(100)
208
+ })
209
+
210
+ it('should compare performance to targets', async () => {
211
+ const result = await trackPerformance({
212
+ executionId: 'exec-111',
213
+ metrics: { duration: 3000 },
214
+ targets: { duration: 2000 },
215
+ })
216
+
217
+ expect(result.comparison.duration.exceeded).toBe(true)
218
+ expect(result.comparison.duration.percentage).toBe(50) // 50% over target
219
+ })
220
+
221
+ it('should detect performance anomalies', async () => {
222
+ const result = await trackPerformance({
223
+ executionId: 'exec-222',
224
+ metrics: { duration: 10000 },
225
+ anomalyDetection: {
226
+ enabled: true,
227
+ baseline: { duration: { mean: 2000, stdDev: 500 } },
228
+ threshold: 3, // 3 standard deviations
229
+ },
230
+ })
231
+
232
+ expect(result.anomalies).toContainEqual(
233
+ expect.objectContaining({
234
+ metric: 'duration',
235
+ deviation: expect.any(Number),
236
+ })
237
+ )
238
+ })
239
+ })
240
+
241
+ describe('Metrics Aggregation', () => {
242
+ it('should aggregate metrics by time period', async () => {
243
+ const metrics = [
244
+ { timestamp: new Date('2024-01-01'), value: 100 },
245
+ { timestamp: new Date('2024-01-01'), value: 200 },
246
+ { timestamp: new Date('2024-01-02'), value: 150 },
247
+ { timestamp: new Date('2024-01-02'), value: 250 },
248
+ ]
249
+
250
+ const aggregated = await aggregateMetrics(metrics, {
251
+ groupBy: 'day',
252
+ aggregation: 'avg',
253
+ })
254
+
255
+ expect(aggregated).toHaveLength(2)
256
+ expect(aggregated[0].value).toBe(150) // avg of 100, 200
257
+ expect(aggregated[1].value).toBe(200) // avg of 150, 250
258
+ })
259
+
260
+ it('should aggregate metrics by dimension', async () => {
261
+ const metrics = [
262
+ { region: 'us-east', value: 100 },
263
+ { region: 'us-east', value: 200 },
264
+ { region: 'eu-west', value: 150 },
265
+ ]
266
+
267
+ const aggregated = await aggregateMetrics(metrics, {
268
+ groupBy: 'region',
269
+ aggregation: 'sum',
270
+ })
271
+
272
+ expect(aggregated.find(m => m.region === 'us-east')?.value).toBe(300)
273
+ expect(aggregated.find(m => m.region === 'eu-west')?.value).toBe(150)
274
+ })
275
+
276
+ it('should calculate percentiles', async () => {
277
+ const metrics = Array.from({ length: 100 }, (_, i) => ({
278
+ value: i + 1, // 1-100
279
+ }))
280
+
281
+ const aggregated = await aggregateMetrics(metrics, {
282
+ aggregation: 'percentiles',
283
+ percentiles: [50, 90, 95, 99],
284
+ })
285
+
286
+ expect(aggregated.p50).toBe(50)
287
+ expect(aggregated.p90).toBe(90)
288
+ expect(aggregated.p95).toBe(95)
289
+ expect(aggregated.p99).toBe(99)
290
+ })
291
+
292
+ it('should calculate rate of change', async () => {
293
+ const metrics = [
294
+ { timestamp: new Date('2024-01-01'), value: 100 },
295
+ { timestamp: new Date('2024-01-02'), value: 120 },
296
+ { timestamp: new Date('2024-01-03'), value: 150 },
297
+ ]
298
+
299
+ const aggregated = await aggregateMetrics(metrics, {
300
+ aggregation: 'rate',
301
+ })
302
+
303
+ expect(aggregated.changeRate).toBeCloseTo(0.25) // 50% increase over 2 days = 25% daily
304
+ })
305
+ })
306
+
307
+ describe('Quality Gates', () => {
308
+ it('should create a quality gate', () => {
309
+ const gate = createQualityGate({
310
+ name: 'Production Ready',
311
+ stage: 'delivery',
312
+ criteria: [
313
+ { metric: 'accuracy', threshold: 0.95, comparison: '>=' },
314
+ { metric: 'testCoverage', threshold: 80, comparison: '>=' },
315
+ { metric: 'errorRate', threshold: 0.01, comparison: '<=' },
316
+ ],
317
+ })
318
+
319
+ expect(gate.name).toBe('Production Ready')
320
+ expect(gate.criteria).toHaveLength(3)
321
+ })
322
+
323
+ it('should evaluate quality gate pass', async () => {
324
+ const gate = createQualityGate({
325
+ name: 'Basic QA',
326
+ criteria: [
327
+ { metric: 'accuracy', threshold: 0.9, comparison: '>=' },
328
+ ],
329
+ })
330
+
331
+ const result = await evaluateQuality(gate, {
332
+ accuracy: 0.95,
333
+ })
334
+
335
+ expect(result.passed).toBe(true)
336
+ expect(result.results[0].passed).toBe(true)
337
+ })
338
+
339
+ it('should evaluate quality gate failure', async () => {
340
+ const gate = createQualityGate({
341
+ name: 'Strict QA',
342
+ criteria: [
343
+ { metric: 'accuracy', threshold: 0.95, comparison: '>=' },
344
+ { metric: 'completeness', threshold: 0.90, comparison: '>=' },
345
+ ],
346
+ })
347
+
348
+ const result = await evaluateQuality(gate, {
349
+ accuracy: 0.92, // below threshold
350
+ completeness: 0.95,
351
+ })
352
+
353
+ expect(result.passed).toBe(false)
354
+ expect(result.failedCriteria).toContain('accuracy')
355
+ })
356
+
357
+ it('should support weighted criteria', async () => {
358
+ const gate = createQualityGate({
359
+ name: 'Weighted QA',
360
+ criteria: [
361
+ { metric: 'accuracy', threshold: 0.9, weight: 0.5 },
362
+ { metric: 'speed', threshold: 1000, comparison: '<=', weight: 0.3 },
363
+ { metric: 'cost', threshold: 0.10, comparison: '<=', weight: 0.2 },
364
+ ],
365
+ passingScore: 0.8, // 80% weighted score to pass
366
+ })
367
+
368
+ const result = await evaluateQuality(gate, {
369
+ accuracy: 0.95, // passes
370
+ speed: 800, // passes
371
+ cost: 0.15, // fails
372
+ })
373
+
374
+ // Score: 0.5 + 0.3 + 0 = 0.8, exactly at threshold
375
+ expect(result.score).toBeCloseTo(0.8)
376
+ expect(result.passed).toBe(true)
377
+ })
378
+
379
+ it('should handle required vs optional criteria', async () => {
380
+ const gate = createQualityGate({
381
+ name: 'Mixed QA',
382
+ criteria: [
383
+ { metric: 'security', threshold: 1.0, required: true },
384
+ { metric: 'performance', threshold: 0.9, required: false },
385
+ ],
386
+ })
387
+
388
+ // Fails required criteria
389
+ let result = await evaluateQuality(gate, {
390
+ security: 0.95,
391
+ performance: 0.95,
392
+ })
393
+ expect(result.passed).toBe(false)
394
+
395
+ // Passes required, fails optional
396
+ result = await evaluateQuality(gate, {
397
+ security: 1.0,
398
+ performance: 0.85,
399
+ })
400
+ expect(result.passed).toBe(true)
401
+ expect(result.warnings).toContain('performance')
402
+ })
403
+ })
404
+
405
+ describe('Quality Dimensions', () => {
406
+ it('should evaluate accuracy dimension', async () => {
407
+ const score = await evaluateQuality.dimension('accuracy', {
408
+ expected: 'The capital of France is Paris.',
409
+ actual: 'Paris is the capital of France.',
410
+ evaluator: 'semantic', // semantic similarity
411
+ })
412
+
413
+ expect(score.value).toBeGreaterThan(0.9)
414
+ })
415
+
416
+ it('should evaluate completeness dimension', async () => {
417
+ const score = await evaluateQuality.dimension('completeness', {
418
+ requirements: ['introduction', 'body', 'conclusion', 'references'],
419
+ deliverables: ['introduction', 'body', 'conclusion'],
420
+ })
421
+
422
+ expect(score.value).toBe(0.75) // 3/4 complete
423
+ expect(score.missing).toContain('references')
424
+ })
425
+
426
+ it('should evaluate timeliness dimension', async () => {
427
+ const score = await evaluateQuality.dimension('timeliness', {
428
+ deadline: new Date('2024-01-15'),
429
+ completedAt: new Date('2024-01-14'),
430
+ })
431
+
432
+ expect(score.value).toBe(1.0) // on time
433
+ expect(score.onTime).toBe(true)
434
+ })
435
+
436
+ it('should evaluate customer satisfaction', async () => {
437
+ const score = await evaluateQuality.dimension('customerSatisfaction', {
438
+ ratings: [5, 4, 5, 4, 5, 3, 4, 5],
439
+ feedback: ['Great work!', 'Very satisfied', 'Minor issues but overall good'],
440
+ })
441
+
442
+ expect(score.value).toBeCloseTo(0.875) // avg 4.375 out of 5
443
+ expect(score.nps).toBeDefined()
444
+ })
445
+ })
446
+
447
+ describe('Metrics Dashboard', () => {
448
+ it('should generate metrics dashboard data', async () => {
449
+ const tracker = createMetricsTracker({ serviceId: 'test-service' })
450
+
451
+ const dashboard = await tracker.getDashboard({
452
+ timeRange: '7d',
453
+ metrics: ['requests', 'latency', 'errors', 'quality'],
454
+ })
455
+
456
+ expect(dashboard.summary).toBeDefined()
457
+ expect(dashboard.charts).toBeDefined()
458
+ expect(dashboard.trends).toBeDefined()
459
+ })
460
+
461
+ it('should calculate health score', async () => {
462
+ const tracker = createMetricsTracker({ serviceId: 'test-service' })
463
+
464
+ const health = await tracker.getHealthScore({
465
+ weights: {
466
+ uptime: 0.3,
467
+ performance: 0.3,
468
+ quality: 0.2,
469
+ customerSatisfaction: 0.2,
470
+ },
471
+ })
472
+
473
+ expect(health.score).toBeGreaterThanOrEqual(0)
474
+ expect(health.score).toBeLessThanOrEqual(1)
475
+ expect(health.status).toMatch(/healthy|degraded|unhealthy/)
476
+ })
477
+ })
478
+ })