sla.test.ts73 lines · main
| 1 | import { describe, expect, test } from 'bun:test'; |
| 2 | |
| 3 | import { creditPercentForBreach, previousMonthBounds, SLA_TIERS } from './sla.js'; |
| 4 | |
| 5 | describe('SLA_TIERS', () => { |
| 6 | test('free tier has no SLA — no auto-credit ever fires', () => { |
| 7 | expect(SLA_TIERS.free).toBeNull(); |
| 8 | }); |
| 9 | |
| 10 | test('pro tier targets 99.5% uptime', () => { |
| 11 | expect(SLA_TIERS.pro?.targetUptime).toBe(0.995); |
| 12 | }); |
| 13 | |
| 14 | test('team tier targets 99.9% uptime — more than 2x stricter than pro', () => { |
| 15 | expect(SLA_TIERS.team?.targetUptime).toBe(0.999); |
| 16 | const proDowntime = 1 - SLA_TIERS.pro!.targetUptime; |
| 17 | const teamDowntime = 1 - SLA_TIERS.team!.targetUptime; |
| 18 | expect(proDowntime / teamDowntime).toBeCloseTo(5, 0); // pro allows 5x the downtime |
| 19 | }); |
| 20 | |
| 21 | test('downtime budgets are sane for a 30-day month', () => { |
| 22 | const monthSeconds = 30 * 86400; |
| 23 | // Pro: 0.5% of a month ≈ 3h 36m |
| 24 | expect(SLA_TIERS.pro!.maxMonthlyDowntimeSeconds).toBeGreaterThan(12_000); |
| 25 | expect(SLA_TIERS.pro!.maxMonthlyDowntimeSeconds).toBeLessThan(monthSeconds * 0.01); |
| 26 | // Team: 0.1% of a month ≈ 43m |
| 27 | expect(SLA_TIERS.team!.maxMonthlyDowntimeSeconds).toBeGreaterThan(2_000); |
| 28 | expect(SLA_TIERS.team!.maxMonthlyDowntimeSeconds).toBeLessThan(3_000); |
| 29 | }); |
| 30 | }); |
| 31 | |
| 32 | describe('creditPercentForBreach', () => { |
| 33 | test('no breach → no credit', () => { |
| 34 | expect(creditPercentForBreach(0.995, 0.999)).toBe(0); |
| 35 | expect(creditPercentForBreach(0.995, 0.995)).toBe(0); |
| 36 | }); |
| 37 | |
| 38 | test('small breach (< 0.5% over the threshold) → 10% credit', () => { |
| 39 | expect(creditPercentForBreach(0.995, 0.993)).toBe(0.1); |
| 40 | expect(creditPercentForBreach(0.995, 0.991)).toBe(0.1); |
| 41 | }); |
| 42 | |
| 43 | test('moderate breach (0.5%–1%) → 25% credit', () => { |
| 44 | expect(creditPercentForBreach(0.995, 0.989)).toBe(0.25); |
| 45 | expect(creditPercentForBreach(0.995, 0.986)).toBe(0.25); |
| 46 | }); |
| 47 | |
| 48 | test('substantial breach (1%–5%) → 50% credit', () => { |
| 49 | expect(creditPercentForBreach(0.995, 0.98)).toBe(0.5); |
| 50 | expect(creditPercentForBreach(0.995, 0.95)).toBe(0.5); |
| 51 | }); |
| 52 | |
| 53 | test('major outage (> 5% breach) → 100% credit (full month free)', () => { |
| 54 | expect(creditPercentForBreach(0.995, 0.94)).toBe(1); |
| 55 | expect(creditPercentForBreach(0.999, 0.85)).toBe(1); |
| 56 | }); |
| 57 | }); |
| 58 | |
| 59 | describe('previousMonthBounds', () => { |
| 60 | test('mid-month March → all of February', () => { |
| 61 | const now = new Date('2026-03-15T10:00:00Z'); |
| 62 | const { start, end } = previousMonthBounds(now); |
| 63 | expect(start.toISOString()).toBe('2026-02-01T00:00:00.000Z'); |
| 64 | expect(end.toISOString()).toBe('2026-03-01T00:00:00.000Z'); |
| 65 | }); |
| 66 | |
| 67 | test('first of January → all of December last year', () => { |
| 68 | const now = new Date('2026-01-01T00:00:00Z'); |
| 69 | const { start, end } = previousMonthBounds(now); |
| 70 | expect(start.toISOString()).toBe('2025-12-01T00:00:00.000Z'); |
| 71 | expect(end.toISOString()).toBe('2026-01-01T00:00:00.000Z'); |
| 72 | }); |
| 73 | }); |