import test from 'node:test'; import assert from 'node:assert/strict'; import { DatabaseSync } from 'node:sqlite'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { loadConfig } from '../server/config.mjs'; import { openStore } from '../server/store.mjs'; import { nextState } from '../server/probe.mjs'; import { makeSnapshot } from '../server/snapshot.mjs'; import { demoSnapshot } from '../server/demo.mjs'; import { createStatusServer } from '../server/index.mjs'; import { unionIntervals, intersectIntervals, subtractIntervals } from '../server/reliability.mjs'; const base = loadConfig({}); const config = { ...base, failureThreshold: 1, recoveryThreshold: 1, staleAfterSeconds: 3, scenarioIntervalSeconds: 1 }; const website = base.services.find(service => service.id === 'website'); const api = base.services.find(service => service.id === 'api'); const good = { ok: true, latencyMs: 100, httpCode: 200, reason: null }; const bad = { ok: false, latencyMs: null, httpCode: 503, reason: 'http' }; function record(store, service, result, time, settings = config) { store.record(service, result, nextState(store.state(service), result, settings), time); } function scenario(store, result, time) { store.recordScenario(api, { ...result, steps: [], mode: 'http' }, nextState(store.scenario(api), result, config), time); } test('interval arithmetic merges overlaps once and preserves excluded gaps', () => { assert.deepEqual(unionIntervals([[4, 8], [1, 5], [8, 10], [15, 14], [12, 13]]), [[1, 10], [12, 13]]); assert.deepEqual(intersectIntervals([[0, 10]], [[1, 3], [5, 12]]), [[1, 3], [5, 10]]); assert.deepEqual(subtractIntervals([[0, 10], [20, 30]], [[-1, 2], [4, 6], [8, 22]]), [[2, 4], [6, 8], [22, 30]]); }); test('outage time starts at confirmation and includes recovery confirmation', () => { const store = openStore(':memory:'); const thresholds = { ...config, failureThreshold: 3, recoveryThreshold: 2 }; try { for (const time of [1000, 2000, 3000]) record(store, website, bad, time, thresholds); for (const time of [4000, 5000]) record(store, website, good, time, thresholds); const stats = store.reliability(website, 0, 6000, config); assert.equal(stats.downtimeMs, 2000); assert.equal(stats.longestDowntimeMs, 2000); assert.equal(stats.incidentCount, 1); assert.equal(stats.lastRecoveryAt, 5000); assert.equal(stats.downtimeIncomplete, false); } finally { store.close(); } }); test('availability and journey outages overlap without double counting', () => { const store = openStore(':memory:'); try { record(store, api, bad, 1000); record(store, api, bad, 2000); record(store, api, good, 4000); scenario(store, good, 1000); scenario(store, bad, 2000); scenario(store, bad, 3000); scenario(store, good, 5000); const stats = store.reliability(api, 0, 6000, config); assert.equal(stats.downtimeMs, 4000); assert.equal(stats.longestDowntimeMs, 3000); assert.equal(stats.incidentCount, 2); assert.equal(stats.lastRecoveryAt, 5000); assert.equal(stats.lastRecoveryScope, 'scenario'); assert.equal(stats.observedMs, 5000); } finally { store.close(); } }); test('monitoring gaps expire at the freshness boundary instead of becoming downtime', () => { const store = openStore(':memory:'); try { record(store, website, bad, 1000); record(store, website, bad, 10000); record(store, website, good, 11000); const stats = store.reliability(website, 0, 12000, config); assert.equal(stats.downtimeMs, 4000); assert.equal(stats.longestDowntimeMs, 4000); assert.equal(stats.downtimeIncomplete, true); assert.equal(stats.observedMs, 5000); const clipped = store.reliability(website, 2000, 3000, config); assert.equal(clipped.downtimeMs, 1000); } finally { store.close(); } }); test('maintenance is subtracted from a genuine incident and planned-only failures do not create outage time', () => { const store = openStore(':memory:'); const window = { titleRu: 'Работы', titleEn: 'Work', messageRu: 'Текст', messageEn: 'Text', serviceIds: ['website'], startsAt: 2000, endsAt: 4000 }; try { store.createMaintenance(window, 0); for (const time of [1000, 2000, 3000, 4000, 5000]) record(store, website, bad, time); record(store, website, good, 6000); const stats = store.reliability(website, 0, 7000, config); assert.equal(stats.downtimeMs, 3000); assert.equal(stats.longestDowntimeMs, 3000); assert.equal(stats.downtimeIncomplete, false); const other = openStore(':memory:'); try { other.createMaintenance({ ...window, startsAt: 1000, endsAt: 3000 }, 0); record(other, website, bad, 1000); record(other, website, bad, 2000); assert.equal(other.reliability(website, 0, 4000, config).downtimeMs, 0); assert.equal(other.reliability(website, 0, 4000, config).incidentCount, 0); } finally { other.close(); } } finally { store.close(); } }); test('an unresolved outage stops accumulating measured time when monitoring stops', () => { const store = openStore(':memory:'); try { record(store, website, bad, 1000); assert.equal(store.reliability(website, 0, 50000, config).downtimeMs, 3000); assert.equal(store.reliability(website, 0, 90000, config).downtimeMs, 3000); assert.equal(store.reliability(website, 0, 90000, config).downtimeIncomplete, true); } finally { store.close(); } }); test('manual reports, unconfigured checks and a changed signature cannot fabricate measured outages', () => { const store = openStore(':memory:'); try { store.createIncident({ serviceId: 'website', titleRu: 'Сообщение', titleEn: 'Report', messageRu: 'Текст', messageEn: 'Text' }, 1000); record(store, website, good, 1000); assert.equal(store.reliability(website, 0, 2000, config).incidentCount, 0); assert.equal(store.reliability({ ...website, signature: 'new' }, 0, 2000, config).downtimeMs, null); const unknown = { ...bad, reason: 'unconfigured' }; const other = { ...website, signature: 'unconfigured' }; record(store, other, unknown, 1000); const stats = store.reliability(other, 0, 2000, config); assert.equal(stats.downtimeMs, null); assert.equal(stats.observedMs, 0); } finally { store.close(); } }); test('an unchecked required journey cannot be presented as zero downtime', () => { const store = openStore(':memory:'); try { record(store, api, good, 1000); scenario(store, { ...bad, ok: null, reason: 'auth_required' }, 1000); const stats = store.reliability(api, 0, 2000, config); assert.equal(stats.downtimeMs, null); assert.equal(stats.longestDowntimeMs, null); assert.equal(stats.incidentCount, 0); assert.equal(stats.observedMs, 0); } finally { store.close(); } }); test('old journey state is retained but historical scenario measurements are not invented on migration', () => { const dir = mkdtempSync(join(tmpdir(), 'ridebus-statistics-')); const path = join(dir, 'legacy.sqlite'); try { const old = openStore(path); scenario(old, bad, 1000); scenario(old, good, 2000); old.close(); const db = new DatabaseSync(path); db.exec('DROP TABLE scenario_checks'); db.close(); const store = openStore(path); try { record(store, api, good, 1000); const stats = store.reliability(api, 0, 3000, config); assert.equal(store.scenario(api).status, 'operational'); assert.equal(stats.incidentCount, 1); assert.equal(stats.downtimeMs, null); assert.equal(stats.downtimeIncomplete, true); assert.equal(stats.lastRecoveryAt, 2000); } finally { store.close(); } } finally { rmSync(dir, { recursive: true, force: true }); } }); test('latency percentile uses successful samples and comparison needs populated windows', () => { const store = openStore(':memory:'); try { for (let index = 0; index < 100; index++) { record(store, website, good, index * 100); record(store, website, { ...good, latencyMs: index + 1 }, 10000 + index * 100); } record(store, website, bad, 19999); record(store, website, { ...good, latencyMs: 9999 }, 30000); const stats = store.latencyDetails(website, 10000, 20000); assert.equal(stats.samples, 100); assert.equal(stats.averageMs, 51); assert.equal(stats.p95Ms, 95); assert.equal(stats.previousAverageMs, 100); assert.equal(stats.changePercent, -49.5); assert.equal(store.latencyDetails(website, 30000, 30001).changePercent, null); assert.equal(store.latencyDetails({ ...website, signature: 'missing' }, 0, 30000).p95Ms, null); for (const time of [0, 500000, 999999, 1000000, 1500000, 1999999]) record(store, website, good, time); assert.equal(store.latencyDetails(website, 1000000, 2000000).changePercent, null); } finally { store.close(); } }); test('public statistics remain scoped, bounded and present for all demo ranges', () => { const store = openStore(':memory:'); try { const snapshot = makeSnapshot(store, base, 30, Date.UTC(2026, 9, 13)); assert.equal(snapshot.services[0].reliability.downtimeMs, null); assert.equal(snapshot.services[0].latencyHistory.length, 30); assert.equal(snapshot.services[0].latency24h.samples, 0); assert.doesNotMatch(JSON.stringify(snapshot), /signature|apiKey|cookie/); for (const range of [7, 30, 90]) { const demo = demoSnapshot(range, Date.UTC(2026, 9, 13)); assert.equal(demo.demo, true); assert.equal(demo.services[0].latencyHistory.length, range); assert.equal(demo.services.find(service => service.id === 'pwa').reliability.downtimeMs, 2100000); } } finally { store.close(); } }); test('internal snapshot cache expires immediately after local and CLI database writes', async () => { const dir = mkdtempSync(join(tmpdir(), 'ridebus-statistics-cache-')); const path = join(dir, 'status.sqlite'); const app = await createStatusServer(loadConfig({ STATUS_DB_PATH: path, PORT: '0', HOST: '127.0.0.1' }), { monitorEnabled: false }); const text = { serviceId: 'website', titleRu: 'Сообщение', titleEn: 'Report', messageRu: 'Текст', messageEn: 'Text' }; try { await new Promise(resolve => app.server.listen(0, '127.0.0.1', resolve)); const url = `http://127.0.0.1:${app.server.address().port}/api/status?range=30`; const initial = await (await fetch(url)).json(); assert.deepEqual(await (await fetch(url)).json(), initial); record(app.store, website, good, Date.now(), base); assert.equal((await (await fetch(url)).json()).services.find(service => service.id === 'website').checks, 1); scenario(app.store, good, Date.now()); assert.equal((await (await fetch(url)).json()).services.find(service => service.id === 'api').scenario.status, 'operational'); app.store.createIncident(text); assert.equal((await (await fetch(url)).json()).incidents.length, 1); const cli = openStore(path); cli.createIncident(text); cli.close(); const response = await fetch(url); assert.equal(response.headers.get('cache-control'), 'no-store'); assert.equal((await response.json()).incidents.length, 2); } finally { await app.close(); rmSync(dir, { recursive: true, force: true }); } });