First public release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env*
|
||||
!.env.example
|
||||
.npmrc
|
||||
dist-cjs/
|
||||
@@ -0,0 +1,6 @@
|
||||
stages: [test]
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: node:22-alpine
|
||||
script: [npm ci, npm run build, npm test]
|
||||
@@ -0,0 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 0.3.0 - 2026-09-28
|
||||
|
||||
First public release under MIT.
|
||||
|
||||
- `slaReport()` computes availability over a half-open window, charges
|
||||
downtime per failing sample capped at one probe interval, and excludes
|
||||
scheduled maintenance.
|
||||
- Zero samples gives `uptime_pct: null`, never 100. No target gives
|
||||
`met: null`, never a pass or a failure.
|
||||
- `toReport()` and `formatSla()` produce an `@extant2000/evidence-record`
|
||||
report. Zero samples with a target is not assessed, not a missed SLA.
|
||||
- `describeAvailability()` and `describeTarget()` put the null cases into
|
||||
plain sentences.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Contributing
|
||||
|
||||
Issues and merge requests are welcome at
|
||||
https://gitlab.com/extant2000/proof-of-up.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- **A test that cannot fail proves nothing.** If you fix a bug, add a test
|
||||
that fails without your fix, and check that it does fail before you submit.
|
||||
- Measure, do not assume. Two modules with the same line count can be
|
||||
different programs.
|
||||
- Keep dependencies minimal. Every new dependency needs a reason.
|
||||
- Explain why in comments, not what.
|
||||
|
||||
## Before you open a merge request
|
||||
|
||||
Run both of these and make sure they pass:
|
||||
|
||||
```
|
||||
npm run build
|
||||
npm test
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Extant 2000 LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,97 @@
|
||||
# ProofOfUp
|
||||
|
||||
Computes availability for uptime and SLA statements from probe samples, and
|
||||
refuses to report a number the samples do not support. It is used in Hitt
|
||||
Hosting products.
|
||||
|
||||
## What it does and why
|
||||
|
||||
An uptime figure is only as good as the samples behind it, and the common
|
||||
failures all flatter the service. ProofOfUp computes availability over
|
||||
samples you have already loaded and handles the edge cases in the direction
|
||||
of saying less.
|
||||
|
||||
**Zero samples is not 100%.** With no samples, `uptime_pct` is `null`, and
|
||||
the report says availability cannot be computed. A statement that answers
|
||||
"100%" when nothing was measured is worse than one that abstains. It also
|
||||
does not say the SLA was missed: when a target applies but nothing was
|
||||
measured, the report is not assessed and states that the target was neither
|
||||
met nor missed. (The raw `met` field stays `false` in that case.)
|
||||
|
||||
"No SLA target" is not "SLA met". With no target, `met` is `null` and the
|
||||
report says no SLA target applies to this plan, so a free plan never gets a
|
||||
statement that reads as a failure.
|
||||
|
||||
The rest of the arithmetic:
|
||||
|
||||
- the window is half-open, `[from, to)`, so adjacent monthly statements never
|
||||
count a boundary sample twice;
|
||||
- samples outside the window or with bad timestamps are dropped, not counted;
|
||||
- each failing sample is charged the gap to the next sample, capped at one
|
||||
probe interval (300 seconds by default), so a long reporting gap after one
|
||||
failure cannot inflate downtime; the last sample is charged one interval;
|
||||
- samples inside scheduled maintenance windows are removed from the math as if
|
||||
the probe never ran; cancelled windows are ignored, overlapping windows are
|
||||
merged, and windows are clipped to the period;
|
||||
- uptime is rounded to one decimal, and the result does not depend on the
|
||||
order of the input.
|
||||
|
||||
The functions are pure, with no I/O and no clock, so the same samples always
|
||||
give the same numbers. That is what makes an availability figure evidence and
|
||||
not a reading taken at a moment. Reports use the `@extant2000/evidence-record`
|
||||
format. The availability citation carries its sample count, so 100% over zero
|
||||
samples cannot be mistaken for 100% over thousands, and excluded maintenance
|
||||
is reported as not applicable with its own measurement so planned downtime
|
||||
stays visible.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { slaReport, formatSla, describeAvailability, describeTarget, type Check } from '@extant2000/proof-of-up'
|
||||
|
||||
// One probe result every five minutes, loaded from wherever you store them.
|
||||
const checks: Check[] = [
|
||||
{ ts: '2026-08-01T00:00:00Z', ok: true },
|
||||
{ ts: '2026-08-01T00:05:00Z', ok: false },
|
||||
// ...
|
||||
]
|
||||
|
||||
const report = slaReport(checks, {
|
||||
from: '2026-08-01T00:00:00Z',
|
||||
to: '2026-09-01T00:00:00Z',
|
||||
target: 99.9, // omit or null for no SLA
|
||||
intervalSeconds: 300,
|
||||
excludeWindows: [{ starts_at: '2026-08-10T02:00:00Z', ends_at: '2026-08-10T03:00:00Z' }],
|
||||
})
|
||||
|
||||
describeAvailability(report) // e.g. 'Availability 99.8% over 8916 samples, 100 minutes down.'
|
||||
describeTarget(report) // e.g. 'Target 99.9% was NOT met.'
|
||||
console.log(formatSla(report, { subject: 'app.example.com' }))
|
||||
```
|
||||
|
||||
For a month of five-minute samples with 20 failures and one hour of
|
||||
maintenance, `formatSla()` printed this (header line omitted):
|
||||
|
||||
```
|
||||
NOT MET · 8916 items examined · 2 findings
|
||||
Availability 99.8% over 8916 samples, 100 minutes down.
|
||||
Target 99.9% was NOT met.
|
||||
|
||||
✗ CRITICAL Availability 99.8% over 8916 samples, 100 minutes down. [NOT MET]
|
||||
Target 99.9% was NOT met.
|
||||
evidence: Availability = 99.8% (8916 samples, 2026-08-01 → 2026-09-01)
|
||||
◦ info 60 minutes of scheduled maintenance excluded (12 samples) [N/A]
|
||||
```
|
||||
|
||||
With no samples, the same call reports NOT ASSESSED and "No samples in this
|
||||
period; availability cannot be computed."
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install @extant2000/proof-of-up
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT. See [LICENSE](LICENSE).
|
||||
@@ -0,0 +1,6 @@
|
||||
# Security
|
||||
|
||||
Please report vulnerabilities privately by email to security@extant2000.com.
|
||||
Do not open a public issue for a security problem.
|
||||
|
||||
We aim to acknowledge every report within 5 business days.
|
||||
Generated
+1504
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@extant2000/proof-of-up",
|
||||
"version": "0.3.0",
|
||||
"private": false,
|
||||
"description": "Backs uptime claims with measured data.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "./dist-cjs/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"dist-cjs",
|
||||
"src",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitlab.com/extant2000/proof-of-up.git"
|
||||
},
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist-cjs/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist-cjs/package.json', JSON.stringify({type:'commonjs'}))\"",
|
||||
"test": "vitest run",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0"
|
||||
},
|
||||
"module": "./dist/index.js",
|
||||
"dependencies": {
|
||||
"@extant2000/evidence-record": "^0.1.2"
|
||||
},
|
||||
"author": "Extant 2000 LLC",
|
||||
"homepage": "https://gitlab.com/extant2000/proof-of-up",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/extant2000/proof-of-up/-/issues"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './sla.js'
|
||||
export { toReport, formatSla, describeAvailability, describeTarget } from './report.js'
|
||||
export type { SlaReportOptions } from './report.js'
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import {
|
||||
type CapabilityReport,
|
||||
type Finding,
|
||||
type FormatOptions,
|
||||
evidence,
|
||||
formatReport as renderReport,
|
||||
} from '@extant2000/evidence-record'
|
||||
import type { SlaReport } from './sla.js'
|
||||
|
||||
export interface SlaReportOptions {
|
||||
/** What the statement is about, e.g. "app.example.com". */
|
||||
subject?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Say the null cases out loud.
|
||||
*
|
||||
* `uptime null, met=false` was correct and unreadable. Nobody should have to
|
||||
* know that zero samples is not 100% in order to read the line, and nobody
|
||||
* should have to know that a null target is not a missed target. The rigour
|
||||
* was already in the arithmetic; this is the phrasing catching up.
|
||||
*/
|
||||
export function describeAvailability(r: SlaReport): string {
|
||||
if (r.uptime_pct === null) {
|
||||
return 'No samples in this period; availability cannot be computed.'
|
||||
}
|
||||
return `Availability ${r.uptime_pct}% over ${r.total_checks} samples, ${r.downtime_minutes} minutes down.`
|
||||
}
|
||||
|
||||
export function describeTarget(r: SlaReport): string {
|
||||
if (r.target_pct === null) return 'No SLA target applies to this plan.'
|
||||
if (r.uptime_pct === null) {
|
||||
return `A ${r.target_pct}% target applies, but nothing was measured — the target was neither met nor missed.`
|
||||
}
|
||||
return r.met
|
||||
? `Target ${r.target_pct}% was met.`
|
||||
: `Target ${r.target_pct}% was NOT met.`
|
||||
}
|
||||
|
||||
const window = (r: SlaReport) =>
|
||||
r.from && r.to ? `${r.from.slice(0, 10)} → ${r.to.slice(0, 10)}` : 'unbounded period'
|
||||
|
||||
/**
|
||||
* Build the portfolio-standard report for one availability statement.
|
||||
*
|
||||
* Zero samples with a target set produces `not-assessed`, not `fail`. The
|
||||
* underlying `met: false` is preserved because the ported arithmetic is
|
||||
* unchanged, but reporting a MISSED SLA when nothing was measured is an
|
||||
* overclaim in the opposite direction from reporting 100% — and this library
|
||||
* exists to avoid exactly that kind of unearned certainty.
|
||||
*/
|
||||
export function toReport(r: SlaReport, opts: SlaReportOptions = {}): CapabilityReport {
|
||||
const subject = opts.subject ?? 'service'
|
||||
const findings: Finding[] = []
|
||||
|
||||
const measured = evidence.measurement('Availability', r.uptime_pct, r.total_checks, {
|
||||
unit: '%',
|
||||
window: window(r),
|
||||
})
|
||||
|
||||
findings.push({
|
||||
id: 'availability',
|
||||
summary: describeAvailability(r),
|
||||
determination: r.uptime_pct === null
|
||||
? 'not-assessed'
|
||||
: r.target_pct === null
|
||||
? 'not-applicable'
|
||||
: r.met ? 'pass' : 'fail',
|
||||
severity: r.uptime_pct === null ? 'high' : r.met === false ? 'critical' : 'info',
|
||||
detail: describeTarget(r),
|
||||
evidence: r.uptime_pct === null ? [] : [measured],
|
||||
})
|
||||
|
||||
if (r.excluded_checks > 0) {
|
||||
findings.push({
|
||||
id: 'maintenance',
|
||||
summary: `${r.excluded_minutes} minutes of scheduled maintenance excluded (${r.excluded_checks} samples)`,
|
||||
determination: 'not-applicable',
|
||||
severity: 'info',
|
||||
detail: 'Planned downtime inside a declared window is removed from the availability math, as if the probe never ran. A cancelled window does not excuse anything.',
|
||||
evidence: [evidence.measurement('Excluded maintenance', r.excluded_minutes, r.excluded_checks, {
|
||||
unit: ' min',
|
||||
window: window(r),
|
||||
})],
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
capability: 'ProofOfUp',
|
||||
scope: `${subject}, ${window(r)}`,
|
||||
// The sample count IS the examined count. Zero samples therefore forces
|
||||
// not-assessed at the report level too, without a special case.
|
||||
examined: r.total_checks,
|
||||
findings,
|
||||
// Both sentences go in the notes, not only on the finding: with zero
|
||||
// samples the renderer leads with "nothing was assessed" and stops before
|
||||
// the findings, and the reader still has to be told why.
|
||||
notes: [describeAvailability(r), describeTarget(r)],
|
||||
}
|
||||
}
|
||||
|
||||
/** Render an availability statement for a terminal, CI log or customer PDF. */
|
||||
export function formatSla(
|
||||
r: SlaReport,
|
||||
opts: SlaReportOptions & FormatOptions = {},
|
||||
): string {
|
||||
return renderReport(toReport(r, opts), opts)
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Availability math for uptime / SLA statements.
|
||||
*
|
||||
* Only the arithmetic lives here. Queries, auth, CSV and printable rendering
|
||||
* stay in the product that calls it.
|
||||
*/
|
||||
|
||||
export interface Check {
|
||||
/** Sample timestamp. */
|
||||
ts: Date | string | number
|
||||
/** Whether the probe succeeded. */
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
export interface MaintenanceWindow {
|
||||
starts_at?: Date | string | number
|
||||
ends_at?: Date | string | number
|
||||
/** Aliases accepted for convenience. */
|
||||
start?: Date | string | number
|
||||
end?: Date | string | number
|
||||
/** A `cancelled` window is ignored — it did not happen. */
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface SlaOptions {
|
||||
from: Date | string | number
|
||||
to: Date | string | number
|
||||
/** Marketed availability target, e.g. 99.9. `null`/absent = no SLA. */
|
||||
target?: number | null
|
||||
/** Probe cadence in seconds. Bounds each failing sample's charged downtime. */
|
||||
intervalSeconds?: number
|
||||
excludeWindows?: MaintenanceWindow[]
|
||||
}
|
||||
|
||||
export interface SlaReport {
|
||||
from: string | null
|
||||
to: string | null
|
||||
total_checks: number
|
||||
up_checks: number
|
||||
down_checks: number
|
||||
/** `null` when there were no samples — NOT 100. */
|
||||
uptime_pct: number | null
|
||||
downtime_minutes: number
|
||||
excluded_checks: number
|
||||
excluded_minutes: number
|
||||
target_pct: number | null
|
||||
/** `null` when no target is set — "no SLA" is not "SLA met". */
|
||||
met: boolean | null
|
||||
}
|
||||
|
||||
function ms(v: unknown): number {
|
||||
if (v instanceof Date) return v.getTime()
|
||||
return new Date(v as string).getTime()
|
||||
}
|
||||
|
||||
function round1(n: number): number {
|
||||
return Math.round(n * 10) / 10
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute an availability report over already-loaded probe samples.
|
||||
*
|
||||
* Pure: no I/O, no clock. Given the same samples it returns the same numbers,
|
||||
* which is what makes an availability figure auditable evidence rather than a
|
||||
* reading taken at a moment.
|
||||
*
|
||||
* The window is half-open `[from, to)` — includes `from`, excludes `to` — so
|
||||
* adjacent monthly statements never double-count a boundary sample.
|
||||
*/
|
||||
export function slaReport(checks: Check[], opts: SlaOptions): SlaReport {
|
||||
const fromMs = ms(opts.from)
|
||||
const toMs = ms(opts.to)
|
||||
|
||||
const target = opts.target != null && Number.isFinite(Number(opts.target))
|
||||
? Number(opts.target)
|
||||
: null
|
||||
|
||||
const intervalSec = Number.isFinite(Number(opts.intervalSeconds)) && Number(opts.intervalSeconds) > 0
|
||||
? Number(opts.intervalSeconds)
|
||||
: 300
|
||||
const intervalMs = intervalSec * 1000
|
||||
|
||||
const fromIso = Number.isFinite(fromMs) ? new Date(fromMs).toISOString() : null
|
||||
const toIso = Number.isFinite(toMs) ? new Date(toMs).toISOString() : null
|
||||
|
||||
// ── Scheduled-maintenance exclusion ──────────────────────────────────────
|
||||
// Normalize windows into half-open [s, e) intervals clipped to the report
|
||||
// period, dropping cancelled ones. A sample inside such an interval is
|
||||
// removed from the availability math entirely — as if the probe never ran —
|
||||
// so planned downtime never counts against uptime_pct.
|
||||
const raw: Array<[number, number]> = []
|
||||
if (Array.isArray(opts.excludeWindows) && Number.isFinite(fromMs) && Number.isFinite(toMs)) {
|
||||
for (const w of opts.excludeWindows) {
|
||||
if (!w || w.status === 'cancelled') continue
|
||||
const s = ms(w.starts_at ?? w.start)
|
||||
const e = ms(w.ends_at ?? w.end)
|
||||
if (!Number.isFinite(s) || !Number.isFinite(e) || e <= s) continue
|
||||
const cs = Math.max(s, fromMs)
|
||||
const ce = Math.min(e, toMs)
|
||||
if (ce > cs) raw.push([cs, ce])
|
||||
}
|
||||
}
|
||||
|
||||
// Merge overlapping/adjacent intervals so excluded_minutes never
|
||||
// double-counts two windows that overlap.
|
||||
raw.sort((a, b) => a[0] - b[0])
|
||||
const merged: Array<[number, number]> = []
|
||||
for (const iv of raw) {
|
||||
const last = merged[merged.length - 1]
|
||||
if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1])
|
||||
else merged.push([iv[0], iv[1]])
|
||||
}
|
||||
const isExcluded = (t: number) => merged.some(([s, e]) => t >= s && t < e)
|
||||
const excludedMs = merged.reduce((acc, [s, e]) => acc + (e - s), 0)
|
||||
|
||||
// ── Collect in-window samples ────────────────────────────────────────────
|
||||
const kept: Array<{ t: number, ok: boolean }> = []
|
||||
let excludedChecks = 0
|
||||
if (Array.isArray(checks) && Number.isFinite(fromMs) && Number.isFinite(toMs)) {
|
||||
for (const c of checks) {
|
||||
if (!c || c.ts == null) continue
|
||||
const t = ms(c.ts)
|
||||
if (!Number.isFinite(t) || t < fromMs || t >= toMs) continue
|
||||
if (isExcluded(t)) { excludedChecks++; continue }
|
||||
kept.push({ t, ok: !!c.ok })
|
||||
}
|
||||
}
|
||||
kept.sort((a, b) => a.t - b.t)
|
||||
|
||||
// ── Availability ─────────────────────────────────────────────────────────
|
||||
let up = 0
|
||||
let downtimeMs = 0
|
||||
for (let i = 0; i < kept.length; i++) {
|
||||
const cur = kept[i]!
|
||||
if (cur.ok) { up++; continue }
|
||||
// Charge the gap to the next sample, capped at one probe interval, so a
|
||||
// long reporting gap after a single failure cannot inflate downtime. The
|
||||
// last sample has no successor and is charged exactly one interval.
|
||||
const next = kept[i + 1]
|
||||
const gap = next ? next.t - cur.t : intervalMs
|
||||
downtimeMs += Math.min(Math.max(gap, 0), intervalMs)
|
||||
}
|
||||
|
||||
const total = kept.length
|
||||
// null, not 100: zero samples is no evidence, and an availability report that
|
||||
// answers "100%" when nothing was measured is worse than one that abstains.
|
||||
const pct = total === 0 ? null : round1((up / total) * 100)
|
||||
|
||||
return {
|
||||
from: fromIso,
|
||||
to: toIso,
|
||||
total_checks: total,
|
||||
up_checks: up,
|
||||
down_checks: total - up,
|
||||
uptime_pct: pct,
|
||||
downtime_minutes: round1(downtimeMs / 60000),
|
||||
excluded_checks: excludedChecks,
|
||||
excluded_minutes: round1(excludedMs / 60000),
|
||||
target_pct: target,
|
||||
// null, not false: "no SLA target" and "SLA missed" are different claims,
|
||||
// and a free plan must not produce a statement that reads as a failure.
|
||||
met: target == null ? null : (pct != null && pct >= target),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { slaReport, toReport, formatSla, type Check } from '../src/index.js'
|
||||
import { overall, validateReport } from '@extant2000/evidence-record'
|
||||
|
||||
const FROM = '2026-07-01T00:00:00.000Z'
|
||||
const TO = '2026-08-01T00:00:00.000Z'
|
||||
|
||||
/** n samples 5 minutes apart from `start`, `okAt` decides pass/fail. */
|
||||
function series(start: string, n: number, okAt: (i: number) => boolean): Check[] {
|
||||
const t0 = new Date(start).getTime()
|
||||
return Array.from({ length: n }, (_, i) => ({
|
||||
ts: new Date(t0 + i * 300_000).toISOString(),
|
||||
ok: okAt(i),
|
||||
}))
|
||||
}
|
||||
|
||||
describe('slaReport — refusing flattering answers', () => {
|
||||
it('reports uptime_pct as NULL with no samples, not 100', () => {
|
||||
// An availability report that answers "100%" when nothing was measured is
|
||||
// worse than one that abstains.
|
||||
const r = slaReport([], { from: FROM, to: TO, target: 99.9 })
|
||||
expect(r.uptime_pct).toBeNull()
|
||||
expect(r.total_checks).toBe(0)
|
||||
})
|
||||
|
||||
it('reports met as NULL with no target, not false', () => {
|
||||
// "no SLA target" and "SLA missed" are different claims. A free plan must
|
||||
// not produce a statement that reads as a failure.
|
||||
expect(slaReport(series(FROM, 10, () => true), { from: FROM, to: TO }).met).toBeNull()
|
||||
})
|
||||
|
||||
it('cannot report met=true from zero evidence', () => {
|
||||
const r = slaReport([], { from: FROM, to: TO, target: 99.9 })
|
||||
expect(r.met).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('slaReport — window semantics', () => {
|
||||
it('is half-open [from, to) so adjacent months never double-count', () => {
|
||||
const boundary: Check[] = [
|
||||
{ ts: FROM, ok: true }, // included
|
||||
{ ts: TO, ok: true }, // excluded — belongs to the NEXT statement
|
||||
]
|
||||
expect(slaReport(boundary, { from: FROM, to: TO }).total_checks).toBe(1)
|
||||
})
|
||||
|
||||
it('drops samples outside the window entirely', () => {
|
||||
const r = slaReport([
|
||||
{ ts: '2026-06-30T23:59:59.000Z', ok: false },
|
||||
{ ts: '2026-07-15T00:00:00.000Z', ok: true },
|
||||
], { from: FROM, to: TO })
|
||||
expect(r.total_checks).toBe(1)
|
||||
expect(r.uptime_pct).toBe(100)
|
||||
})
|
||||
|
||||
it('ignores malformed samples rather than counting them', () => {
|
||||
const r = slaReport(
|
||||
[{ ts: 'not-a-date', ok: false }, { ts: FROM, ok: true }] as Check[],
|
||||
{ from: FROM, to: TO },
|
||||
)
|
||||
expect(r.total_checks).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('slaReport — availability and downtime', () => {
|
||||
it('computes uptime to one decimal', () => {
|
||||
// 999 up, 1 down out of 1000 → 99.9
|
||||
const r = slaReport(series(FROM, 1000, i => i !== 500), { from: FROM, to: TO, target: 99.9 })
|
||||
expect(r.uptime_pct).toBe(99.9)
|
||||
expect(r.up_checks).toBe(999)
|
||||
expect(r.down_checks).toBe(1)
|
||||
expect(r.met).toBe(true)
|
||||
})
|
||||
|
||||
it('misses the target just below it', () => {
|
||||
// 998/1000 = 99.8 < 99.9
|
||||
const r = slaReport(series(FROM, 1000, i => i !== 500 && i !== 600), {
|
||||
from: FROM, to: TO, target: 99.9,
|
||||
})
|
||||
expect(r.uptime_pct).toBe(99.8)
|
||||
expect(r.met).toBe(false)
|
||||
})
|
||||
|
||||
it('charges a failing sample the gap to its successor, capped at one interval', () => {
|
||||
// Two failures 5 min apart at a 300s cadence → 5 min each = 10 min.
|
||||
const r = slaReport(series(FROM, 3, i => i === 2), {
|
||||
from: FROM, to: TO, intervalSeconds: 300,
|
||||
})
|
||||
expect(r.downtime_minutes).toBe(10)
|
||||
})
|
||||
|
||||
it('caps a long reporting gap at one interval', () => {
|
||||
// A single failure followed by a sample a DAY later must not charge a day.
|
||||
const r = slaReport([
|
||||
{ ts: '2026-07-01T00:00:00.000Z', ok: false },
|
||||
{ ts: '2026-07-02T00:00:00.000Z', ok: true },
|
||||
], { from: FROM, to: TO, intervalSeconds: 300 })
|
||||
expect(r.downtime_minutes).toBe(5)
|
||||
})
|
||||
|
||||
it('charges the final sample exactly one interval when it has no successor', () => {
|
||||
const r = slaReport([{ ts: FROM, ok: false }], {
|
||||
from: FROM, to: TO, intervalSeconds: 600,
|
||||
})
|
||||
expect(r.downtime_minutes).toBe(10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('slaReport — scheduled maintenance', () => {
|
||||
const win = [{ starts_at: '2026-07-10T00:00:00.000Z', ends_at: '2026-07-10T01:00:00.000Z' }]
|
||||
|
||||
it('removes samples inside a window from the math entirely', () => {
|
||||
const r = slaReport([
|
||||
{ ts: '2026-07-10T00:30:00.000Z', ok: false }, // planned downtime
|
||||
{ ts: '2026-07-11T00:00:00.000Z', ok: true },
|
||||
], { from: FROM, to: TO, excludeWindows: win, target: 99.9 })
|
||||
|
||||
expect(r.excluded_checks).toBe(1)
|
||||
expect(r.total_checks).toBe(1)
|
||||
expect(r.uptime_pct).toBe(100) // planned downtime never counts against it
|
||||
expect(r.excluded_minutes).toBe(60)
|
||||
})
|
||||
|
||||
it('ignores a cancelled window — it did not happen', () => {
|
||||
const r = slaReport([{ ts: '2026-07-10T00:30:00.000Z', ok: false }], {
|
||||
from: FROM, to: TO,
|
||||
excludeWindows: [{ ...win[0]!, status: 'cancelled' }],
|
||||
})
|
||||
expect(r.excluded_checks).toBe(0)
|
||||
expect(r.uptime_pct).toBe(0)
|
||||
})
|
||||
|
||||
it('does NOT double-count overlapping windows', () => {
|
||||
const r = slaReport([], {
|
||||
from: FROM, to: TO,
|
||||
excludeWindows: [
|
||||
{ starts_at: '2026-07-10T00:00:00.000Z', ends_at: '2026-07-10T02:00:00.000Z' },
|
||||
{ starts_at: '2026-07-10T01:00:00.000Z', ends_at: '2026-07-10T03:00:00.000Z' },
|
||||
],
|
||||
})
|
||||
expect(r.excluded_minutes).toBe(180) // union, not 120 + 120
|
||||
})
|
||||
|
||||
it('clips a window to the report period', () => {
|
||||
const r = slaReport([], {
|
||||
from: FROM, to: TO,
|
||||
excludeWindows: [{ starts_at: '2026-06-01T00:00:00.000Z', ends_at: '2026-07-01T01:00:00.000Z' }],
|
||||
})
|
||||
expect(r.excluded_minutes).toBe(60) // only the hour inside July
|
||||
})
|
||||
|
||||
it('drops a zero-length or inverted window', () => {
|
||||
const r = slaReport([], {
|
||||
from: FROM, to: TO,
|
||||
excludeWindows: [
|
||||
{ starts_at: '2026-07-10T01:00:00.000Z', ends_at: '2026-07-10T01:00:00.000Z' },
|
||||
{ starts_at: '2026-07-10T05:00:00.000Z', ends_at: '2026-07-10T04:00:00.000Z' },
|
||||
],
|
||||
})
|
||||
expect(r.excluded_minutes).toBe(0)
|
||||
})
|
||||
|
||||
it('accepts start/end aliases', () => {
|
||||
const r = slaReport([], {
|
||||
from: FROM, to: TO,
|
||||
excludeWindows: [{ start: '2026-07-10T00:00:00.000Z', end: '2026-07-10T00:30:00.000Z' }],
|
||||
})
|
||||
expect(r.excluded_minutes).toBe(30)
|
||||
})
|
||||
})
|
||||
|
||||
describe('slaReport — reproducibility', () => {
|
||||
it('same input, identical output', () => {
|
||||
const checks = series(FROM, 200, i => i % 17 !== 0)
|
||||
const a = slaReport(checks, { from: FROM, to: TO, target: 99 })
|
||||
const b = slaReport(checks, { from: FROM, to: TO, target: 99 })
|
||||
expect(a).toEqual(b)
|
||||
})
|
||||
|
||||
it('is order-independent — unsorted input gives the same answer', () => {
|
||||
const checks = series(FROM, 50, i => i !== 10)
|
||||
const shuffled = [...checks].reverse()
|
||||
expect(slaReport(shuffled, { from: FROM, to: TO, intervalSeconds: 300 }))
|
||||
.toEqual(slaReport(checks, { from: FROM, to: TO, intervalSeconds: 300 }))
|
||||
})
|
||||
})
|
||||
|
||||
describe('report — the null cases said out loud', () => {
|
||||
const period = { from: '2026-07-01T00:00:00Z', to: '2026-08-01T00:00:00Z' }
|
||||
|
||||
it('reads zero samples as uncomputable, not as a missed SLA', () => {
|
||||
// `uptime null, met=false` is correct arithmetic and an overclaim to
|
||||
// print: nothing was measured, so nothing was missed.
|
||||
const r = slaReport([], { ...period, target: 99.9 })
|
||||
expect(r.uptime_pct).toBeNull()
|
||||
expect(r.met).toBe(false) // ported behaviour, unchanged
|
||||
expect(overall(toReport(r))).toBe('not-assessed') // what a reader is told
|
||||
const text = formatSla(r)
|
||||
expect(text).toContain('No samples in this period; availability cannot be computed.')
|
||||
expect(text).toContain('neither met nor missed')
|
||||
expect(text).not.toContain('100%')
|
||||
})
|
||||
|
||||
it('says plainly when no SLA target applies', () => {
|
||||
const r = slaReport([{ ts: '2026-07-02T00:00:00Z', ok: true }], period)
|
||||
expect(r.met).toBeNull()
|
||||
expect(formatSla(r)).toContain('No SLA target applies to this plan.')
|
||||
})
|
||||
|
||||
it('carries the sample count into the citation', () => {
|
||||
const checks = Array.from({ length: 100 }, (_, i) => ({
|
||||
ts: new Date(Date.parse(period.from) + i * 300_000).toISOString(),
|
||||
ok: i !== 3,
|
||||
}))
|
||||
const r = slaReport(checks, { ...period, target: 99.9 })
|
||||
const text = formatSla(r, { subject: 'app.example.com', evidence: 'inline' })
|
||||
expect(text).toContain('100 samples')
|
||||
expect(text).toContain('app.example.com')
|
||||
expect(overall(toReport(r))).toBe('fail')
|
||||
expect(text).toContain('was NOT met')
|
||||
})
|
||||
|
||||
it('reports excluded maintenance as not-applicable, with its own measurement', () => {
|
||||
const checks = Array.from({ length: 20 }, (_, i) => ({
|
||||
ts: new Date(Date.parse(period.from) + i * 300_000).toISOString(),
|
||||
ok: true,
|
||||
}))
|
||||
const r = slaReport(checks, {
|
||||
...period,
|
||||
target: 99.9,
|
||||
excludeWindows: [{ start: period.from, end: new Date(Date.parse(period.from) + 3600_000).toISOString() }],
|
||||
})
|
||||
expect(r.excluded_checks).toBeGreaterThan(0)
|
||||
const f = toReport(r).findings.find(x => x.id === 'maintenance')!
|
||||
expect(f.determination).toBe('not-applicable')
|
||||
expect(formatSla(r)).toContain('scheduled maintenance excluded')
|
||||
})
|
||||
|
||||
it('every conclusion carries a citation', () => {
|
||||
const r = slaReport([{ ts: '2026-07-02T00:00:00Z', ok: true }], { ...period, target: 99.9 })
|
||||
expect(validateReport(toReport(r))).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist-cjs",
|
||||
"declaration": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
|
||||
"declaration": true, "outDir": "dist", "rootDir": "src",
|
||||
"strict": true, "skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user