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,414 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SLA (Service Level Agreement) Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for defining and monitoring service level agreements.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
8
|
+
import {
|
|
9
|
+
defineSLA,
|
|
10
|
+
createSLO,
|
|
11
|
+
monitorSLA,
|
|
12
|
+
checkSLACompliance,
|
|
13
|
+
calculateErrorBudget,
|
|
14
|
+
createSLAAlert,
|
|
15
|
+
recordSLABreach,
|
|
16
|
+
getSLAReport,
|
|
17
|
+
} from '../src/sla/index.js'
|
|
18
|
+
|
|
19
|
+
describe('Service Level Agreements', () => {
|
|
20
|
+
describe('defineSLA', () => {
|
|
21
|
+
it('should define a basic SLA', () => {
|
|
22
|
+
const sla = defineSLA({
|
|
23
|
+
name: 'Standard SLA',
|
|
24
|
+
version: '1.0',
|
|
25
|
+
tier: 'standard',
|
|
26
|
+
uptimeTarget: 99.9,
|
|
27
|
+
responseTimeTarget: 200, // ms
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(sla.name).toBe('Standard SLA')
|
|
31
|
+
expect(sla.tier).toBe('standard')
|
|
32
|
+
expect(sla.uptimeTarget).toBe(99.9)
|
|
33
|
+
expect(sla.responseTimeTarget).toBe(200)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should define SLA with support levels', () => {
|
|
37
|
+
const sla = defineSLA({
|
|
38
|
+
name: 'Premium SLA',
|
|
39
|
+
tier: 'premium',
|
|
40
|
+
support: {
|
|
41
|
+
hours: '24/7',
|
|
42
|
+
channels: ['email', 'chat', 'phone'],
|
|
43
|
+
responseTime: {
|
|
44
|
+
critical: '15 minutes',
|
|
45
|
+
high: '1 hour',
|
|
46
|
+
medium: '4 hours',
|
|
47
|
+
low: '24 hours',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
expect(sla.support.hours).toBe('24/7')
|
|
53
|
+
expect(sla.support.channels).toContain('phone')
|
|
54
|
+
expect(sla.support.responseTime.critical).toBe('15 minutes')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should define SLA with credit policy', () => {
|
|
58
|
+
const sla = defineSLA({
|
|
59
|
+
name: 'Enterprise SLA',
|
|
60
|
+
tier: 'enterprise',
|
|
61
|
+
uptimeTarget: 99.99,
|
|
62
|
+
creditPolicy: {
|
|
63
|
+
'99.99-99.9': 10, // 10% credit for 99.9-99.99% uptime
|
|
64
|
+
'99.9-99.0': 25, // 25% credit
|
|
65
|
+
'below-99.0': 50, // 50% credit
|
|
66
|
+
},
|
|
67
|
+
maxCredit: 100,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(sla.creditPolicy['99.99-99.9']).toBe(10)
|
|
71
|
+
expect(sla.maxCredit).toBe(100)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('should define SLA with exclusions', () => {
|
|
75
|
+
const sla = defineSLA({
|
|
76
|
+
name: 'Standard SLA',
|
|
77
|
+
tier: 'standard',
|
|
78
|
+
exclusions: [
|
|
79
|
+
'Scheduled maintenance',
|
|
80
|
+
'Force majeure',
|
|
81
|
+
'Customer-caused issues',
|
|
82
|
+
],
|
|
83
|
+
maintenanceWindows: [
|
|
84
|
+
{ day: 'Sunday', time: '02:00-06:00', timezone: 'UTC' },
|
|
85
|
+
],
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
expect(sla.exclusions).toContain('Scheduled maintenance')
|
|
89
|
+
expect(sla.maintenanceWindows).toHaveLength(1)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('should define SLA with effective dates', () => {
|
|
93
|
+
const sla = defineSLA({
|
|
94
|
+
name: 'Q1 SLA',
|
|
95
|
+
tier: 'standard',
|
|
96
|
+
effectiveFrom: new Date('2024-01-01'),
|
|
97
|
+
effectiveUntil: new Date('2024-03-31'),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
expect(sla.effectiveFrom).toEqual(new Date('2024-01-01'))
|
|
101
|
+
expect(sla.effectiveUntil).toEqual(new Date('2024-03-31'))
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('createSLO', () => {
|
|
106
|
+
it('should create an SLO (Service Level Objective)', () => {
|
|
107
|
+
const slo = createSLO({
|
|
108
|
+
name: 'API Availability',
|
|
109
|
+
metric: 'availability',
|
|
110
|
+
target: 99.9,
|
|
111
|
+
comparison: '>=',
|
|
112
|
+
windowType: 'rolling',
|
|
113
|
+
windowDuration: '30d',
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(slo.name).toBe('API Availability')
|
|
117
|
+
expect(slo.metric).toBe('availability')
|
|
118
|
+
expect(slo.target).toBe(99.9)
|
|
119
|
+
expect(slo.windowDuration).toBe('30d')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should create SLO with error budget', () => {
|
|
123
|
+
const slo = createSLO({
|
|
124
|
+
name: 'Latency P99',
|
|
125
|
+
metric: 'latency',
|
|
126
|
+
target: 200, // 200ms
|
|
127
|
+
unit: 'ms',
|
|
128
|
+
errorBudget: 0.1, // 0.1% error budget
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
expect(slo.errorBudget).toBe(0.1)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should create SLO with alerting config', () => {
|
|
135
|
+
const slo = createSLO({
|
|
136
|
+
name: 'Error Rate',
|
|
137
|
+
metric: 'error-rate',
|
|
138
|
+
target: 0.1, // 0.1%
|
|
139
|
+
comparison: '<=',
|
|
140
|
+
alertThreshold: 0.08, // Alert at 0.08%
|
|
141
|
+
alertOnBreach: true,
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
expect(slo.alertThreshold).toBe(0.08)
|
|
145
|
+
expect(slo.alertOnBreach).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
describe('monitorSLA', () => {
|
|
150
|
+
it('should start monitoring an SLA', async () => {
|
|
151
|
+
const sla = defineSLA({
|
|
152
|
+
name: 'Test SLA',
|
|
153
|
+
tier: 'standard',
|
|
154
|
+
uptimeTarget: 99.9,
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const monitor = await monitorSLA(sla, {
|
|
158
|
+
checkInterval: 60000, // every minute
|
|
159
|
+
metricsSource: async () => ({
|
|
160
|
+
uptime: 99.95,
|
|
161
|
+
responseTime: 150,
|
|
162
|
+
}),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
expect(monitor.status).toBe('monitoring')
|
|
166
|
+
expect(monitor.slaId).toBe(sla.id)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should emit events on SLA status change', async () => {
|
|
170
|
+
const onBreach = vi.fn()
|
|
171
|
+
|
|
172
|
+
const sla = defineSLA({
|
|
173
|
+
name: 'Test SLA',
|
|
174
|
+
tier: 'standard',
|
|
175
|
+
uptimeTarget: 99.9,
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const monitor = await monitorSLA(sla, {
|
|
179
|
+
metricsSource: async () => ({ uptime: 99.0 }), // below target
|
|
180
|
+
onBreach,
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
await monitor.check()
|
|
184
|
+
|
|
185
|
+
expect(onBreach).toHaveBeenCalled()
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should track SLA burn rate', async () => {
|
|
189
|
+
const sla = defineSLA({
|
|
190
|
+
name: 'Test SLA',
|
|
191
|
+
tier: 'standard',
|
|
192
|
+
uptimeTarget: 99.9,
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
const monitor = await monitorSLA(sla, {
|
|
196
|
+
metricsSource: async () => ({ uptime: 99.5 }),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
const stats = await monitor.getStats()
|
|
200
|
+
|
|
201
|
+
expect(stats.burnRate).toBeDefined()
|
|
202
|
+
expect(stats.errorBudgetRemaining).toBeDefined()
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe('checkSLACompliance', () => {
|
|
207
|
+
it('should check if metrics are within SLA', () => {
|
|
208
|
+
const sla = defineSLA({
|
|
209
|
+
name: 'Test SLA',
|
|
210
|
+
tier: 'standard',
|
|
211
|
+
uptimeTarget: 99.9,
|
|
212
|
+
responseTimeTarget: 200,
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
const result = checkSLACompliance(sla, {
|
|
216
|
+
uptime: 99.95,
|
|
217
|
+
responseTime: 150,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
expect(result.compliant).toBe(true)
|
|
221
|
+
expect(result.metrics.uptime.passed).toBe(true)
|
|
222
|
+
expect(result.metrics.responseTime.passed).toBe(true)
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('should detect SLA breach', () => {
|
|
226
|
+
const sla = defineSLA({
|
|
227
|
+
name: 'Test SLA',
|
|
228
|
+
tier: 'standard',
|
|
229
|
+
uptimeTarget: 99.9,
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
const result = checkSLACompliance(sla, {
|
|
233
|
+
uptime: 99.5, // below target
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
expect(result.compliant).toBe(false)
|
|
237
|
+
expect(result.metrics.uptime.passed).toBe(false)
|
|
238
|
+
expect(result.metrics.uptime.deviation).toBe(-0.4)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should exclude maintenance windows from compliance check', () => {
|
|
242
|
+
const sla = defineSLA({
|
|
243
|
+
name: 'Test SLA',
|
|
244
|
+
tier: 'standard',
|
|
245
|
+
uptimeTarget: 99.9,
|
|
246
|
+
maintenanceWindows: [
|
|
247
|
+
{ day: 'Sunday', time: '02:00-06:00', timezone: 'UTC' },
|
|
248
|
+
],
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
const result = checkSLACompliance(sla, {
|
|
252
|
+
uptime: 99.5,
|
|
253
|
+
maintenanceHours: 4, // 4 hours of maintenance
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
// Should adjust uptime calculation to exclude maintenance
|
|
257
|
+
expect(result.adjustedUptime).toBeGreaterThan(99.5)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
describe('calculateErrorBudget', () => {
|
|
262
|
+
it('should calculate remaining error budget', () => {
|
|
263
|
+
const budget = calculateErrorBudget({
|
|
264
|
+
target: 99.9, // 99.9% uptime
|
|
265
|
+
windowDays: 30,
|
|
266
|
+
currentUptime: 99.95,
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
expect(budget.totalBudget).toBe(43.2) // 0.1% of 30 days in minutes
|
|
270
|
+
expect(budget.consumed).toBeLessThan(budget.totalBudget)
|
|
271
|
+
expect(budget.remaining).toBeGreaterThan(0)
|
|
272
|
+
expect(budget.remainingPercentage).toBeGreaterThan(0)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it('should indicate when error budget is exhausted', () => {
|
|
276
|
+
const budget = calculateErrorBudget({
|
|
277
|
+
target: 99.9,
|
|
278
|
+
windowDays: 30,
|
|
279
|
+
currentUptime: 99.0, // significantly below target
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
expect(budget.remaining).toBeLessThanOrEqual(0)
|
|
283
|
+
expect(budget.exhausted).toBe(true)
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
it('should calculate burn rate', () => {
|
|
287
|
+
const budget = calculateErrorBudget({
|
|
288
|
+
target: 99.9,
|
|
289
|
+
windowDays: 30,
|
|
290
|
+
currentUptime: 99.85,
|
|
291
|
+
daysPassed: 15,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
// Burn rate > 1 means consuming budget faster than allowed
|
|
295
|
+
expect(budget.burnRate).toBeDefined()
|
|
296
|
+
})
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
describe('createSLAAlert', () => {
|
|
300
|
+
it('should create alert configuration', () => {
|
|
301
|
+
const alert = createSLAAlert({
|
|
302
|
+
name: 'High Error Rate Alert',
|
|
303
|
+
condition: 'error_rate > 0.1',
|
|
304
|
+
severity: 'critical',
|
|
305
|
+
channels: ['email', 'slack', 'pagerduty'],
|
|
306
|
+
cooldown: 300, // 5 minutes
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
expect(alert.name).toBe('High Error Rate Alert')
|
|
310
|
+
expect(alert.severity).toBe('critical')
|
|
311
|
+
expect(alert.channels).toContain('pagerduty')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should support escalation paths', () => {
|
|
315
|
+
const alert = createSLAAlert({
|
|
316
|
+
name: 'SLA Breach Alert',
|
|
317
|
+
condition: 'uptime < 99.9',
|
|
318
|
+
escalation: [
|
|
319
|
+
{ after: '0m', notify: 'on-call-engineer' },
|
|
320
|
+
{ after: '15m', notify: 'engineering-manager' },
|
|
321
|
+
{ after: '30m', notify: 'vp-engineering' },
|
|
322
|
+
],
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
expect(alert.escalation).toHaveLength(3)
|
|
326
|
+
expect(alert.escalation[1].after).toBe('15m')
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
describe('recordSLABreach', () => {
|
|
331
|
+
it('should record an SLA breach incident', async () => {
|
|
332
|
+
const sla = defineSLA({
|
|
333
|
+
name: 'Test SLA',
|
|
334
|
+
tier: 'standard',
|
|
335
|
+
uptimeTarget: 99.9,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
const breach = await recordSLABreach({
|
|
339
|
+
slaId: sla.id,
|
|
340
|
+
metric: 'uptime',
|
|
341
|
+
expected: 99.9,
|
|
342
|
+
actual: 99.5,
|
|
343
|
+
startedAt: new Date('2024-01-15T10:00:00Z'),
|
|
344
|
+
endedAt: new Date('2024-01-15T12:00:00Z'),
|
|
345
|
+
impact: 'partial',
|
|
346
|
+
affectedCustomers: 150,
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
expect(breach.id).toBeDefined()
|
|
350
|
+
expect(breach.duration).toBe(7200000) // 2 hours in ms
|
|
351
|
+
expect(breach.affectedCustomers).toBe(150)
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('should calculate credit owed', async () => {
|
|
355
|
+
const sla = defineSLA({
|
|
356
|
+
name: 'Test SLA',
|
|
357
|
+
tier: 'enterprise',
|
|
358
|
+
uptimeTarget: 99.99,
|
|
359
|
+
creditPolicy: {
|
|
360
|
+
'99.99-99.9': 10,
|
|
361
|
+
'99.9-99.0': 25,
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
const breach = await recordSLABreach({
|
|
366
|
+
slaId: sla.id,
|
|
367
|
+
metric: 'uptime',
|
|
368
|
+
actual: 99.5,
|
|
369
|
+
monthlyBillingAmount: 10000,
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
expect(breach.creditOwed).toBe(2500) // 25% of $10,000
|
|
373
|
+
})
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
describe('getSLAReport', () => {
|
|
377
|
+
it('should generate SLA compliance report', async () => {
|
|
378
|
+
const sla = defineSLA({
|
|
379
|
+
name: 'Test SLA',
|
|
380
|
+
tier: 'standard',
|
|
381
|
+
uptimeTarget: 99.9,
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
const report = await getSLAReport({
|
|
385
|
+
slaId: sla.id,
|
|
386
|
+
period: {
|
|
387
|
+
start: new Date('2024-01-01'),
|
|
388
|
+
end: new Date('2024-01-31'),
|
|
389
|
+
},
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
expect(report.slaId).toBe(sla.id)
|
|
393
|
+
expect(report.period).toBeDefined()
|
|
394
|
+
expect(report.metrics).toBeDefined()
|
|
395
|
+
expect(report.breaches).toBeDefined()
|
|
396
|
+
expect(report.compliancePercentage).toBeDefined()
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('should include historical trends', async () => {
|
|
400
|
+
const report = await getSLAReport({
|
|
401
|
+
slaId: 'test-sla',
|
|
402
|
+
period: {
|
|
403
|
+
start: new Date('2024-01-01'),
|
|
404
|
+
end: new Date('2024-03-31'),
|
|
405
|
+
},
|
|
406
|
+
includeHistory: true,
|
|
407
|
+
groupBy: 'month',
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
expect(report.history).toHaveLength(3) // 3 months
|
|
411
|
+
expect(report.history[0].month).toBe('2024-01')
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
})
|