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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/package.json +14 -14
- package/tests/communication.test.ts +517 -0
- package/tests/metrics.test.ts +478 -0
- package/tests/pricing.test.ts +297 -0
- package/tests/service-definition.test.ts +208 -0
- package/tests/sla.test.ts +414 -0
- package/tests/workflow.test.ts +569 -0
- package/vitest.config.ts +12 -0
|
@@ -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
|
+
})
|