OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / sla-service.test.js

189 lines · javascript ref:
import test from 'brittle'
import { SLAService } from 'p2p-hiveservices/builtin/sla-service.js'

function mockNode (opts = {}) {
  const slashed = []
  const published = []
  return {
    _proofOfRelay: {
      scores: opts.scores || new Map()
    },
    reputation: {
      getRecord: (pubkey) => opts.reputationRecords?.[pubkey] || null
    },
    paymentManager: {
      slash: (pubkey, amount, reason) => { slashed.push({ pubkey, amount, reason }) }
    },
    router: {
      pubsub: {
        publish: (topic, data) => { published.push({ topic, data }) }
      }
    },
    _slashed: slashed,
    _published: published
  }
}

function createService (nodeOpts = {}) {
  const svc = new SLAService()
  const node = mockNode(nodeOpts)
  svc.start({ node })
  // Clear the enforcement interval for tests (we'll call manually)
  clearInterval(svc._checkInterval)
  svc._checkInterval = null
  return { svc, node }
}

const baseParams = {
  appKey: 'a'.repeat(64),
  relayPubkey: 'b'.repeat(64),
  guarantees: { minReliability: 0.99, maxLatencyMs: 2000 },
  collateral: 10000,
  premiumRate: 3.0,
  duration: 30 * 24 * 60 * 60 * 1000
}

test('SLAService - manifest', async (t) => {
  const svc = new SLAService()
  const m = svc.manifest()
  t.is(m.name, 'sla')
  t.ok(m.capabilities.includes('create'))
  t.ok(m.capabilities.includes('check'))
})

test('SLAService - create contract', async (t) => {
  const { svc } = createService()
  const c = await svc.create(baseParams)
  t.is(c.appKey, baseParams.appKey)
  t.is(c.status, 'active')
  t.is(c.collateral, 10000)
  t.is(c.collateralRemaining, 10000)
  t.ok(c.id.length === 32)
  t.ok(c.violations.length === 0)
})

test('SLAService - create validates required fields', async (t) => {
  const { svc } = createService()
  try { await svc.create({}); t.fail() } catch (e) { t.ok(e.message.includes('SLA_MISSING')) }
  try { await svc.create({ appKey: 'a', relayPubkey: 'b' }); t.fail() } catch (e) { t.ok(e.message.includes('SLA_MISSING')) }
  try { await svc.create({ ...baseParams, collateral: 0 }); t.fail() } catch (e) { t.ok(e.message.includes('SLA_INVALID')) }
})

test('SLAService - get and list', async (t) => {
  const { svc } = createService()
  const c = await svc.create(baseParams)

  const got = await svc.get({ id: c.id })
  t.is(got.id, c.id)

  const list = await svc.list()
  t.is(list.length, 1)

  const filtered = await svc.list({ status: 'terminated' })
  t.is(filtered.length, 0)
})

test('SLAService - get not found', async (t) => {
  const { svc } = createService()
  try { await svc.get({ id: 'nonexistent' }); t.fail() } catch (e) { t.ok(e.message.includes('SLA_NOT_FOUND')) }
})

test('SLAService - terminate', async (t) => {
  const { svc, node } = createService()
  const c = await svc.create(baseParams)

  await svc.terminate({ id: c.id, slashRemaining: true })
  const got = await svc.get({ id: c.id })
  t.is(got.status, 'terminated')
  t.is(got.collateralRemaining, 0)
  t.is(node._slashed.length, 1)
  t.is(node._slashed[0].amount, 10000)
})

test('SLAService - check detects reliability violation', async (t) => {
  const scores = new Map()
  scores.set('b'.repeat(64), { challenges: 100, passes: 90, fails: 10, avgLatencyMs: 500 })
  const { svc, node } = createService({ scores })
  const c = await svc.create(baseParams)

  const result = await svc.check({ id: c.id })
  t.is(result.passed, false)
  t.ok(result.violations.some(v => v.type === 'reliability'))
  t.is(node._slashed.length, 1) // Penalty applied
})

test('SLAService - check detects latency violation', async (t) => {
  const scores = new Map()
  scores.set('b'.repeat(64), { challenges: 100, passes: 100, fails: 0, avgLatencyMs: 3000 })
  const { svc } = createService({ scores })
  const c = await svc.create(baseParams)

  const result = await svc.check({ id: c.id })
  t.is(result.passed, false)
  t.ok(result.violations.some(v => v.type === 'latency'))
})

test('SLAService - check passes when guarantees met', async (t) => {
  const scores = new Map()
  scores.set('b'.repeat(64), { challenges: 100, passes: 100, fails: 0, avgLatencyMs: 500 })
  const { svc } = createService({ scores })
  const c = await svc.create(baseParams)

  const result = await svc.check({ id: c.id })
  t.is(result.passed, true)
  t.is(result.violations.length, 0)
})

test('SLAService - auto-terminate after 3 violations', async (t) => {
  const scores = new Map()
  scores.set('b'.repeat(64), { challenges: 100, passes: 50, fails: 50, avgLatencyMs: 5000 })
  const { svc } = createService({ scores })
  const c = await svc.create(baseParams)

  // Each check produces 2 violations (reliability + latency)
  await svc.check({ id: c.id }) // 2 violations
  const got1 = await svc.get({ id: c.id })
  t.is(got1.status, 'active') // Still active at 2

  await svc.check({ id: c.id }) // 4 violations total, exceeds MAX_VIOLATIONS=3
  const got2 = await svc.get({ id: c.id })
  t.is(got2.status, 'violated')
  t.is(got2.collateralRemaining, 0)
})

test('SLAService - violations returns array', async (t) => {
  const scores = new Map()
  scores.set('b'.repeat(64), { challenges: 100, passes: 90, fails: 10, avgLatencyMs: 500 })
  const { svc } = createService({ scores })
  const c = await svc.create(baseParams)
  await svc.check({ id: c.id })

  const violations = await svc.violations({ id: c.id })
  t.ok(violations.length > 0)
  t.ok(violations[0].timestamp)
  t.ok(violations[0].penalty >= 0)
})

test('SLAService - stats', async (t) => {
  const { svc } = createService()
  await svc.create(baseParams)
  await svc.create({ ...baseParams, collateral: 5000 })

  const s = await svc.stats()
  t.is(s.total, 2)
  t.is(s.active, 2)
  t.is(s.totalCollateral, 15000)
})

test('SLAService - enforceAll expires old contracts', async (t) => {
  const { svc } = createService()
  const c = await svc.create({ ...baseParams, duration: 1 }) // 1ms duration

  // Wait for expiry
  await new Promise(resolve => setTimeout(resolve, 10))
  svc._enforceAll()

  const got = await svc.get({ id: c.id })
  t.is(got.status, 'expired')
})