Updates
CI / test (push) Successful in 58s
Release rolling / release (push) Successful in 7m10s

This commit is contained in:
Raven Scott
2026-07-18 19:16:58 -04:00
parent 1d46b7b3ad
commit 462cce4b5b
24 changed files with 1398 additions and 51 deletions
+42
View File
@@ -0,0 +1,42 @@
import test from 'brittle'
import { scoreSeverity, AnomalyEngine } from '../server/services/anomaly.js'
test('scoreSeverity warning vs critical bands', (t) => {
const warn = scoreSeverity(85, 80, 95, '>', 'warning')
const crit = scoreSeverity(96, 80, 95, '>', 'critical')
t.ok(warn >= 0.35 && warn < 1)
t.ok(crit >= 0.6)
t.ok(crit > warn)
})
test('scoreSeverity low-is-worse', (t) => {
const warn = scoreSeverity(400, 512, 256, '<', 'warning')
const crit = scoreSeverity(200, 512, 256, '<', 'critical')
t.ok(warn > 0)
t.ok(crit >= 0.6)
})
test('evaluate includes continuous score in message', (t) => {
const eng = new AnomalyEngine({ cpuCount: 4 })
eng.setConfig({
id: 'cpu_user_high',
chart: 'system.cpu',
dimension: 'user',
warn: 10,
crit: 50,
comparator: '>',
enabled: true,
info: 'CPU user',
})
const fired = eng.evaluate([
{
chart: 'system.cpu',
context: 'system.cpu',
ts: Date.now(),
values: { user: 60, idle: 40 },
},
])
t.ok(fired.length >= 1)
t.ok(fired[0].score >= 0.6)
t.ok(String(fired[0].message).includes('score'))
})
+37
View File
@@ -0,0 +1,37 @@
import test from 'brittle'
import {
shouldNotify,
meetsMinSeverity,
isWebhookEnabled,
} from '../server/services/notify.js'
test('meetsMinSeverity', (t) => {
const prev = process.env.PEARDATA_NOTIFY_MIN_SEVERITY
delete process.env.PEARDATA_NOTIFY_MIN_SEVERITY
t.ok(meetsMinSeverity('warning'))
t.ok(meetsMinSeverity('critical'))
process.env.PEARDATA_NOTIFY_MIN_SEVERITY = 'critical'
t.absent(meetsMinSeverity('warning'))
t.ok(meetsMinSeverity('critical'))
if (prev == null) delete process.env.PEARDATA_NOTIFY_MIN_SEVERITY
else process.env.PEARDATA_NOTIFY_MIN_SEVERITY = prev
})
test('shouldNotify gates on webhook + clears', (t) => {
const prevUrl = process.env.PEARDATA_WEBHOOK_URL
const prevClears = process.env.PEARDATA_NOTIFY_CLEARS
delete process.env.PEARDATA_WEBHOOK_URL
t.absent(isWebhookEnabled())
t.absent(shouldNotify({ severity: 'critical', cleared: false }))
process.env.PEARDATA_WEBHOOK_URL = 'http://127.0.0.1:9/hook'
t.ok(shouldNotify({ severity: 'warning', cleared: false }))
t.absent(shouldNotify({ severity: 'warning', cleared: true }))
process.env.PEARDATA_NOTIFY_CLEARS = '1'
t.ok(shouldNotify({ severity: 'warning', cleared: true }))
if (prevUrl == null) delete process.env.PEARDATA_WEBHOOK_URL
else process.env.PEARDATA_WEBHOOK_URL = prevUrl
if (prevClears == null) delete process.env.PEARDATA_NOTIFY_CLEARS
else process.env.PEARDATA_NOTIFY_CLEARS = prevClears
})
+44
View File
@@ -0,0 +1,44 @@
import test from 'brittle'
import {
remapDockChart,
extractDockCharts,
isPearDockEnabled,
parsePearDockPeers,
} from '../server/services/collectors/peardock.js'
test('remapDockChart', (t) => {
t.is(remapDockChart('docker.cpu.abc'), 'peardock.cpu.abc')
t.is(remapDockChart('docker.containers'), 'peardock.containers')
t.is(remapDockChart('container.mem.x'), 'peardock.mem.x')
t.is(remapDockChart('system.cpu'), null)
})
test('extractDockCharts from getAllMetrics body', (t) => {
const rows = extractDockCharts({
body: {
charts: {
'docker.cpu.abc123': { dimensions: { usage: 12.5 } },
'system.cpu': { dimensions: { idle: 90 } },
},
},
})
t.is(rows.length, 1)
t.is(rows[0].chart, 'peardock.cpu.abc123')
t.is(rows[0].context, 'peardock.cpu')
t.is(rows[0].values.usage, 12.5)
})
test('isPearDockEnabled + parse peers', (t) => {
const prev = process.env.PEARDATA_PEARDOCK
const prevPeers = process.env.PEARDATA_PEARDOCK_PEERS
delete process.env.PEARDATA_PEARDOCK
t.absent(isPearDockEnabled())
process.env.PEARDATA_PEARDOCK = '1'
t.ok(isPearDockEnabled())
process.env.PEARDATA_PEARDOCK_PEERS = `${'a'.repeat(64)}, nope`
t.is(parsePearDockPeers().length, 1)
if (prev == null) delete process.env.PEARDATA_PEARDOCK
else process.env.PEARDATA_PEARDOCK = prev
if (prevPeers == null) delete process.env.PEARDATA_PEARDOCK_PEERS
else process.env.PEARDATA_PEARDOCK_PEERS = prevPeers
})
+30
View File
@@ -0,0 +1,30 @@
import test from 'brittle'
import {
isRestTunnelEnabled,
publicKeyFromTunnelSeed,
getRestTunnelInfo,
} from '../server/services/rest-tunnel.js'
test('isRestTunnelEnabled', (t) => {
const prev = process.env.PEARDATA_REST_TUNNEL
delete process.env.PEARDATA_REST_TUNNEL
t.absent(isRestTunnelEnabled())
process.env.PEARDATA_REST_TUNNEL = '1'
t.ok(isRestTunnelEnabled())
if (prev == null) delete process.env.PEARDATA_REST_TUNNEL
else process.env.PEARDATA_REST_TUNNEL = prev
})
test('publicKeyFromTunnelSeed is stable', (t) => {
const seed = 'a'.repeat(64)
const a = publicKeyFromTunnelSeed(seed)
const b = publicKeyFromTunnelSeed(seed)
t.is(a, b)
t.is(a.length, 64)
})
test('getRestTunnelInfo inactive by default', (t) => {
const info = getRestTunnelInfo()
t.ok(typeof info.enabled === 'boolean')
t.absent(info.active)
})
+33
View File
@@ -0,0 +1,33 @@
import test from 'brittle'
import {
signWebhookPayload,
verifyWebhookSignature,
isWebhookSigningEnabled,
} from '../server/services/notify.js'
test('sign and verify webhook HMAC', (t) => {
const secret = 'test-secret'
const ts = '1710000000000'
const body = '{"type":"anomaly","severity":"critical"}'
const sig = signWebhookPayload(secret, ts, body)
t.ok(sig.startsWith('sha256='))
t.ok(verifyWebhookSignature(secret, ts, body, sig, { now: Number(ts) }))
t.absent(verifyWebhookSignature(secret, ts, body + 'x', sig, { now: Number(ts) }))
t.absent(
verifyWebhookSignature(secret, ts, body, sig, { now: Number(ts) + 10 * 60_000 })
)
})
test('isWebhookSigningEnabled defaults on with secret', (t) => {
const prevSecret = process.env.PEARDATA_WEBHOOK_SECRET
const prevSign = process.env.PEARDATA_WEBHOOK_SIGN
process.env.PEARDATA_WEBHOOK_SECRET = 'abc'
delete process.env.PEARDATA_WEBHOOK_SIGN
t.ok(isWebhookSigningEnabled())
process.env.PEARDATA_WEBHOOK_SIGN = '0'
t.absent(isWebhookSigningEnabled())
if (prevSecret == null) delete process.env.PEARDATA_WEBHOOK_SECRET
else process.env.PEARDATA_WEBHOOK_SECRET = prevSecret
if (prevSign == null) delete process.env.PEARDATA_WEBHOOK_SIGN
else process.env.PEARDATA_WEBHOOK_SIGN = prevSign
})
+51
View File
@@ -0,0 +1,51 @@
import test from 'brittle'
import { MetricStore } from '../server/services/store.js'
import { AnomalyEngine } from '../server/services/anomaly.js'
import { stats, zScore, thresholdsFromStats, seriesFromStore } from '../server/services/zscore.js'
test('stats and zScore', (t) => {
const s = stats([10, 12, 11, 13, 10])
t.ok(s.n === 5)
t.ok(Math.abs(s.mean - 11.2) < 0.01)
t.ok(s.stdev > 0)
t.ok(Math.abs(zScore(11.2, s.mean, s.stdev)) < 0.01)
t.ok(zScore(20, s.mean, s.stdev) > 2)
})
test('thresholdsFromStats greater-than', (t) => {
const thr = thresholdsFromStats({ mean: 10, stdev: 2 }, '>', { warnZ: 2, critZ: 3 })
t.is(thr.warn, 14)
t.is(thr.crit, 16)
})
test('retrainAnomaly updates thresholds from store', (t) => {
const store = new MetricStore()
const now = Date.now()
for (let i = 0; i < 40; i++) {
store.ingest([
{
chart: 'system.cpu',
context: 'system.cpu',
ts: now + i * 1000,
values: {
user: 20 + (i % 3),
system: 5,
nice: 0,
iowait: 0,
irq: 0,
softirq: 0,
idle: 70,
steal: 0,
},
},
])
}
const eng = new AnomalyEngine({ cpuCount: 2 })
const res = eng.retrain(store, { minPoints: 20, warnZ: 2, critZ: 3 })
t.ok(res.ok)
t.ok(res.updated.length >= 1)
const cpu = eng.configs.get('cpu_user_high')
t.ok(cpu.warn > 20)
t.ok(cpu.crit >= cpu.warn)
t.ok(seriesFromStore(store, 'system.cpu', 'user').length >= 20)
})