Updates
This commit is contained in:
@@ -40,3 +40,31 @@ test('evaluate includes continuous score in message', (t) => {
|
||||
t.ok(fired[0].score >= 0.6)
|
||||
t.ok(String(fired[0].message).includes('score'))
|
||||
})
|
||||
|
||||
test('getWeights ranks active anomaly charts', (t) => {
|
||||
const eng = new AnomalyEngine({ cpuCount: 2 })
|
||||
eng.setConfig({
|
||||
id: 'load_high',
|
||||
chart: 'system.load',
|
||||
dimension: 'load1',
|
||||
warn: 1,
|
||||
crit: 2,
|
||||
comparator: '>',
|
||||
enabled: true,
|
||||
info: 'Load',
|
||||
})
|
||||
eng.evaluate([
|
||||
{
|
||||
chart: 'system.load',
|
||||
context: 'system.load',
|
||||
ts: Date.now(),
|
||||
values: { load1: 5 },
|
||||
},
|
||||
])
|
||||
const w = eng.getWeights({ limit: 20 })
|
||||
t.ok(w.results?.length >= 1)
|
||||
const row = w.results.find((r) => r.id === 'system.load')
|
||||
t.ok(row)
|
||||
t.ok(row.weight >= 0.65)
|
||||
t.is(row.severity, 'critical')
|
||||
})
|
||||
|
||||
+25
-23
@@ -7,33 +7,35 @@ import {
|
||||
upsertBookmark,
|
||||
removeBookmark,
|
||||
setBookmarkAlias,
|
||||
bookmarksPath,
|
||||
} from '../client/bookmarks.js'
|
||||
|
||||
const tmp = path.join(os.tmpdir(), `peardata-bm-${Date.now()}`)
|
||||
import { getPeersCachePath } from '../client/peerCache.js'
|
||||
|
||||
test('bookmarks upsert alias remove', (t) => {
|
||||
fs.mkdirSync(tmp, { recursive: true })
|
||||
const prev = process.env.PEARDATA_HOME
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'peardata-bm-'))
|
||||
process.env.PEARDATA_HOME = tmp
|
||||
t.ok(bookmarksPath().includes('bookmarks.json'))
|
||||
|
||||
const pk = 'a'.repeat(64)
|
||||
upsertBookmark({ publicKeyHex: pk, alias: 'box-1' })
|
||||
let list = loadBookmarks()
|
||||
t.is(list.length, 1)
|
||||
t.is(list[0].alias, 'box-1')
|
||||
|
||||
setBookmarkAlias(pk, 'box-2')
|
||||
list = loadBookmarks()
|
||||
t.is(list[0].alias, 'box-2')
|
||||
|
||||
removeBookmark(pk)
|
||||
t.is(loadBookmarks().length, 0)
|
||||
|
||||
try {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
} catch {
|
||||
// ignore
|
||||
t.ok(getPeersCachePath().includes(path.join('cache', 'peers.json')))
|
||||
|
||||
const pk = 'a'.repeat(64)
|
||||
upsertBookmark({ publicKeyHex: pk, alias: 'box-1' })
|
||||
let list = loadBookmarks()
|
||||
t.is(list.length, 1)
|
||||
t.is(list[0].alias, 'box-1')
|
||||
|
||||
setBookmarkAlias(pk, 'box-2')
|
||||
list = loadBookmarks()
|
||||
t.is(list[0].alias, 'box-2')
|
||||
|
||||
removeBookmark(pk)
|
||||
t.is(loadBookmarks().length, 0)
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.PEARDATA_HOME
|
||||
else process.env.PEARDATA_HOME = prev
|
||||
try {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
delete process.env.PEARDATA_HOME
|
||||
})
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import test from 'brittle'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import path from 'path'
|
||||
import {
|
||||
normalizePeerEntry,
|
||||
parsePeersPayload,
|
||||
loadPeers,
|
||||
savePeers,
|
||||
upsertPeer,
|
||||
removePeer,
|
||||
getLastActivePeerId,
|
||||
setLastActivePeerId,
|
||||
getPeersCachePath,
|
||||
} from '../client/peerCache.js'
|
||||
|
||||
function withHome(fn) {
|
||||
const prev = process.env.PEARDATA_HOME
|
||||
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'peardata-peers-'))
|
||||
process.env.PEARDATA_HOME = tmp
|
||||
try {
|
||||
return fn(tmp)
|
||||
} finally {
|
||||
if (prev === undefined) delete process.env.PEARDATA_HOME
|
||||
else process.env.PEARDATA_HOME = prev
|
||||
try {
|
||||
fs.rmSync(tmp, { recursive: true, force: true })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test('normalizePeerEntry accepts hex keys', (t) => {
|
||||
const hex = 'a'.repeat(64)
|
||||
const e = normalizePeerEntry({ publicKeyHex: hex, alias: 'prod' })
|
||||
t.is(e.publicKeyHex, hex)
|
||||
t.is(e.alias, 'prod')
|
||||
t.absent(normalizePeerEntry({ publicKeyHex: 'nope' }))
|
||||
})
|
||||
|
||||
test('parsePeersPayload reads envelope + bookmarks legacy', (t) => {
|
||||
const hex = 'b'.repeat(64)
|
||||
const fromEnv = parsePeersPayload({
|
||||
version: 1,
|
||||
peers: { [hex]: { publicKeyHex: hex, alias: 'x' } },
|
||||
})
|
||||
t.ok(fromEnv[hex])
|
||||
const fromBm = parsePeersPayload({
|
||||
bookmarks: [{ publicKeyHex: hex, alias: 'y' }],
|
||||
})
|
||||
t.is(fromBm[hex].alias, 'y')
|
||||
})
|
||||
|
||||
test('upsertPeer + active id round-trip', (t) => {
|
||||
withHome(() => {
|
||||
const hex = 'c'.repeat(64)
|
||||
upsertPeer({ publicKeyHex: hex, alias: 'lab' }, { makeActive: true })
|
||||
const peers = loadPeers()
|
||||
t.is(peers[hex].alias, 'lab')
|
||||
t.is(getLastActivePeerId(), hex)
|
||||
t.ok(fs.existsSync(getPeersCachePath()))
|
||||
|
||||
setLastActivePeerId(null)
|
||||
t.absent(getLastActivePeerId())
|
||||
|
||||
removePeer(hex)
|
||||
t.absent(loadPeers()[hex])
|
||||
})
|
||||
})
|
||||
|
||||
test('savePeers refuses accidental wipe', (t) => {
|
||||
withHome(() => {
|
||||
const hex = 'd'.repeat(64)
|
||||
upsertPeer({ publicKeyHex: hex }, { makeActive: true })
|
||||
const ok = savePeers({})
|
||||
t.is(ok, false)
|
||||
t.ok(loadPeers()[hex], 'peer still present after refused wipe')
|
||||
savePeers({}, { force: true, activePeerId: null })
|
||||
t.is(Object.keys(loadPeers()).length, 0)
|
||||
})
|
||||
})
|
||||
@@ -39,6 +39,7 @@ test('method roles cover monitoring surface', (t) => {
|
||||
'unlinkPeer',
|
||||
'getFleetHealth',
|
||||
'listChildPeers',
|
||||
'getWeights',
|
||||
]) {
|
||||
t.ok(MethodRoles[m], m)
|
||||
t.is(Methods[m], m)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import test from 'brittle'
|
||||
import { pearson, rankRelatedCharts } from '../shared/related-metrics.js'
|
||||
|
||||
test('pearson correlates aligned series', (t) => {
|
||||
const x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
const y = x.map((v) => v * 2)
|
||||
t.ok(pearson(x, y) > 0.99)
|
||||
const z = x.map((v) => -v)
|
||||
t.ok(pearson(x, z) < -0.99)
|
||||
})
|
||||
|
||||
test('rankRelatedCharts prefers same context/family', (t) => {
|
||||
const catalog = {
|
||||
'system.cpu': { context: 'system.cpu', family: 'cpu', plugin: 'proc', units: '%' },
|
||||
'cpu.cpu0': { context: 'cpu.cpu', family: 'cpu', plugin: 'proc', units: '%' },
|
||||
'system.ram': { context: 'system.ram', family: 'ram', plugin: 'proc', units: 'MiB' },
|
||||
'nginx.connections': { context: 'nginx.connections', family: 'nginx', plugin: 'nginx', units: 'connections' },
|
||||
}
|
||||
const ranked = rankRelatedCharts('system.cpu', catalog, null, { limit: 5 })
|
||||
t.ok(ranked.length >= 1)
|
||||
t.is(ranked[0].id, 'cpu.cpu0')
|
||||
t.ok(!ranked.some((r) => r.id === 'nginx.connections') || ranked.at(-1)?.id === 'nginx.connections')
|
||||
})
|
||||
|
||||
test('rankRelatedCharts boosts loaded correlation', (t) => {
|
||||
const series = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
|
||||
const catalog = {
|
||||
a: { context: 'x.a', family: 'f1', plugin: 'p' },
|
||||
b: { context: 'y.b', family: 'f2', plugin: 'q' },
|
||||
c: { context: 'z.c', family: 'f3', plugin: 'r' },
|
||||
}
|
||||
const loaded = new Map([
|
||||
['a', { dims: new Map([['v', series]]) }],
|
||||
['b', { dims: new Map([['v', series.map((n) => n * 3)]]) }],
|
||||
['c', { dims: new Map([['v', series.map((_, i) => (i % 2 ? 10 : 0))]]) }],
|
||||
])
|
||||
const ranked = rankRelatedCharts('a', catalog, loaded, { limit: 5 })
|
||||
t.ok(ranked[0].id === 'b')
|
||||
t.ok(ranked[0].reason.includes('corr'))
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import test from 'brittle'
|
||||
import { getAllChartDefs } from '../shared/metrics.js'
|
||||
import { sectionForChart, SECTIONS } from '../shared/taxonomy.js'
|
||||
|
||||
test('static catalog charts map into taxonomy sections', (t) => {
|
||||
const defs = getAllChartDefs()
|
||||
t.ok(defs.length > 20)
|
||||
/** @type {Record<string, number>} */
|
||||
const counts = Object.fromEntries(SECTIONS.map((s) => [s.id, 0]))
|
||||
let other = 0
|
||||
for (const def of defs) {
|
||||
const sec = sectionForChart(def.id, def)
|
||||
counts[sec.id] = (counts[sec.id] || 0) + 1
|
||||
if (sec.id === 'other') other++
|
||||
}
|
||||
t.ok(counts.system > 0, 'system section should have charts')
|
||||
// Most built-in charts should not dump into Other
|
||||
const ratio = other / defs.length
|
||||
t.ok(ratio < 0.35, `other ratio ${ratio.toFixed(2)} should stay under 35%`)
|
||||
})
|
||||
|
||||
test('common plugin prefixes are classified', (t) => {
|
||||
const samples = [
|
||||
['system.cpu', 'system'],
|
||||
['mem.available', 'system'],
|
||||
['docker.cpu.x', 'containers'],
|
||||
['zfs.arc', 'storage'],
|
||||
['sensors.temp.cpu', 'hardware'],
|
||||
['nginx.connections', 'applications'],
|
||||
['ebpf.cachestat', 'observability'],
|
||||
['fleet.nodes', 'fleet'],
|
||||
]
|
||||
for (const [id, want] of samples) {
|
||||
t.is(sectionForChart(id, { context: id }).id, want, id)
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user