OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / disk-monitor.test.js

148 lines · javascript ref:
// disk-monitor: tests for the operator-facing disk-usage signal added
// in v0.8.28 to close the cost-of-discovery gap that bit milkyb-iad
// (issue #27).

import test from 'brittle'
import { DiskMonitor, parseDfOutput } from 'p2p-hiverelay/core/relay-node/disk-monitor.js'

test('parseDfOutput: well-formed POSIX df -kP output', (t) => {
  // Real df -kP output from a Linux relay
  const stdout = [
    'Filesystem     1024-blocks      Used Available Capacity Mounted on',
    '/dev/sda1         51190108  23456789  25178945      48% /'
  ].join('\n')

  const parsed = parseDfOutput(stdout)
  t.is(parsed.totalBytes, 51190108 * 1024, 'totalBytes converted from 1K blocks')
  t.is(parsed.usedBytes, 23456789 * 1024)
  t.is(parsed.freeBytes, 25178945 * 1024)
  t.is(parsed.mountPath, '/')
})

test('parseDfOutput: filesystem column with spaces (line-wrapped)', (t) => {
  // df sometimes wraps long filesystem paths onto a second line
  const stdout = [
    'Filesystem            1024-blocks      Used Available Capacity Mounted on',
    '/very/long/filesystem/path-that-wraps',
    '                          1048576    524288    524288      50% /data'
  ].join('\n')

  const parsed = parseDfOutput(stdout)
  t.is(parsed.totalBytes, 1048576 * 1024)
  t.is(parsed.usedBytes, 524288 * 1024)
  t.is(parsed.freeBytes, 524288 * 1024)
  t.is(parsed.mountPath, '/data')
})

test('parseDfOutput: throws on malformed output', (t) => {
  t.exception(() => parseDfOutput(''), /no data row/)
  t.exception(() => parseDfOutput('Filesystem ...'), /no data row/)
  t.exception(() => parseDfOutput('Filesystem ...\nshort row'), /fewer than 6 columns/)
})

test('DiskMonitor: status threshold calculations (ok / warn / critical)', async (t) => {
  // Use the milkyb-iad scenario (1 GB volume) at different fill levels
  function mockCache (usedPct) {
    const total = 1024 * 1024 * 1024
    return {
      usedPct,
      usedBytes: Math.floor(total * usedPct / 100),
      freeBytes: total - Math.floor(total * usedPct / 100),
      totalBytes: total
    }
  }

  const dm = new DiskMonitor('/data', { warnThreshold: 85, criticalThreshold: 95 })
  t.teardown(() => dm.stop())

  // Stub _refresh to set the cache directly
  dm._cache = {
    ...mockCache(50),
    mountPath: '/data',
    status: 'ok',
    checkedAt: Date.now()
  }
  t.is(dm.getInfo().status, 'ok', '50% fill is ok')

  dm._cache = {
    ...mockCache(86),
    mountPath: '/data',
    status: 'warn',
    checkedAt: Date.now()
  }
  t.is(dm.getInfo().status, 'warn', '86% fill is warn')

  dm._cache = {
    ...mockCache(95),
    mountPath: '/data',
    status: 'critical',
    checkedAt: Date.now()
  }
  t.is(dm.getInfo().status, 'critical', '95% fill is critical')
})

test('DiskMonitor: getInfo returns null before first poll completes', (t) => {
  const dm = new DiskMonitor('/data')
  t.teardown(() => dm.stop())
  t.is(dm.getInfo(), null, 'cache empty before refresh')
})

test('DiskMonitor: refresh() against real / returns plausible numbers', async (t) => {
  // Smoke test using the actual host filesystem. The relay storage path
  // doesn't exist in this test environment, but / does on Linux + macOS.
  const dm = new DiskMonitor('/')
  t.teardown(() => dm.stop())

  await dm.refresh()
  const info = dm.getInfo()
  t.ok(info, 'cache populated after refresh')
  t.ok(!info.error, 'no error: ' + (info.error || ''))
  t.ok(info.totalBytes > 0, 'total bytes positive')
  t.ok(info.freeBytes >= 0, 'free bytes non-negative')
  t.ok(info.usedPct >= 0 && info.usedPct <= 100, 'usedPct in 0-100')
  t.ok(info.checkedAt > 0, 'checkedAt timestamp set')
  t.is(typeof info.status, 'string')
  t.ok(['ok', 'warn', 'critical'].includes(info.status), 'status is one of the three')
})

test('DiskMonitor: refresh() surfaces error when df fails on bogus path', async (t) => {
  const dm = new DiskMonitor('/this/path/definitely/does/not/exist/abc123xyz')
  t.teardown(() => dm.stop())

  await dm.refresh()
  const info = dm.getInfo()
  t.ok(info, 'cache populated even on error')
  t.ok(info.error, 'error field set')
  t.ok(info.checkedAt > 0)
})

test('DiskMonitor: stop() clears the refresh interval', async (t) => {
  const dm = new DiskMonitor('/', { refreshIntervalMs: 5000 })
  dm.start()
  t.ok(dm._refreshInterval, 'interval armed after start')
  dm.stop()
  t.absent(dm._refreshInterval, 'interval cleared after stop')
  // Calling stop again is a no-op
  dm.stop()
  t.pass('second stop is harmless')
})

test('DiskMonitor: custom thresholds override defaults', (t) => {
  const dm = new DiskMonitor('/data', { warnThreshold: 70, criticalThreshold: 90 })
  t.teardown(() => dm.stop())
  t.is(dm.warnThreshold, 70)
  t.is(dm.criticalThreshold, 90)
})

test('DiskMonitor: requires storagePath', (t) => {
  t.exception(() => new DiskMonitor(), /storagePath is required/)
  t.exception(() => new DiskMonitor(null), /storagePath is required/)
  t.exception(() => new DiskMonitor(''), /storagePath is required/)
})

test('DiskMonitor: refresh interval floor of 5000ms (prevents accidental DoS)', (t) => {
  const dm = new DiskMonitor('/data', { refreshIntervalMs: 100 })
  t.teardown(() => dm.stop())
  t.is(dm.refreshIntervalMs, 5000, 'sub-5s clamped to 5s floor')
})