OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / profile-vector-verifier.test.js

188 lines · javascript ref:
import test from 'brittle'
import { execFile } from 'child_process'
import { mkdtemp, readFile, readdir, rm, writeFile } from 'fs/promises'
import { tmpdir } from 'os'
import path from 'path'
import {
  SUPPORTED_PROFILE_VECTOR_NAMES,
  verifyProfileVector,
  verifyProfileVectors
} from 'p2p-hiverelay/core/protocol/profile-vector-verifier.js'

const FIXTURE_DIR = new URL('../fixtures/relaykernel-profile/', import.meta.url)

async function loadVectors () {
  const dir = FIXTURE_DIR
  const names = (await readdir(dir)).filter(name => name.endsWith('.json')).sort()
  const vectors = []
  for (const name of names) {
    vectors.push(JSON.parse(await readFile(new URL(name, dir), 'utf8')))
  }
  return vectors
}

function runScript (args = []) {
  return new Promise((resolve) => {
    execFile(process.execPath, ['scripts/verify-profile-vectors.mjs', ...args], {
      cwd: process.cwd(),
      env: process.env
    }, (error, stdout, stderr) => {
      resolve({
        code: error && typeof error.code === 'number' ? error.code : 0,
        stdout,
        stderr
      })
    })
  })
}

test('profile vector verifier accepts all checked-in transplant vectors', async (t) => {
  const vectors = await loadVectors()
  const summary = verifyProfileVectors(vectors, { requireSupportedSet: true })
  const names = new Set(vectors.map(vector => vector.name))

  t.ok(vectors.length >= 4, 'fixture set includes all transplant vectors')
  t.is(vectors.length, SUPPORTED_PROFILE_VECTOR_NAMES.length, 'fixture count matches supported verifier table')
  t.ok(SUPPORTED_PROFILE_VECTOR_NAMES.includes('relaykernel-http-route-matrix-v1-blindspark-compat'), 'supported vector inventory includes RelayKernel route matrix vector')
  t.ok(names.has('relaykernel-profile-v1-app-module-boundary'), 'fixture set includes RelayKernel app-module boundary vector')
  t.ok(names.has('capability-doc-signature-v1'), 'fixture set includes signed capability-doc conformance vector')
  t.ok(names.has('relaykernel-http-route-matrix-v1-blindspark-compat'), 'fixture set includes RelayKernel HTTP route matrix vector')
  t.ok(names.has('seed-protocol-binary-v1'), 'fixture set includes seed protocol binary conformance vector')
  t.ok(names.has('seed-protocol-handshake-v1'), 'fixture set includes seed protocol version-negotiation vector')
  t.ok(names.has('circuit-protocol-binary-v1'), 'fixture set includes circuit protocol binary conformance vector')
  t.alike(summary.inventory.names, [...SUPPORTED_PROFILE_VECTOR_NAMES].sort(), 'fixture names match supported verifier table')
  t.alike(summary.inventory.missingNames, [], 'full fixture set has every supported profile vector')
  t.alike(summary.inventory.duplicateNames, [], 'full fixture set has no duplicate profile vector names')
  t.ok(summary.valid, 'all vectors pass')
  t.is(summary.count, vectors.length)
  t.is(summary.failed, 0)
  for (const result of summary.results) t.ok(result.valid, result.name + ' verifies')
})

test('profile vector verifier enforces full fixture inventory only when requested', async (t) => {
  const vectors = await loadVectors()
  const accounting = vectors.find(vector => vector.name === 'accounting-receipt-v1-os-grounded')
  const withoutRouteMatrix = vectors.filter(vector => vector.name !== 'relaykernel-http-route-matrix-v1-blindspark-compat')
  const missing = verifyProfileVectors(withoutRouteMatrix, { requireSupportedSet: true })

  t.absent(missing.valid, 'missing supported fixture fails full inventory')
  t.ok(missing.inventory.errors.includes('missing supported profile vector: relaykernel-http-route-matrix-v1-blindspark-compat'))

  const focused = verifyProfileVectors([accounting])
  t.ok(focused.valid, 'single-file debug verification does not require the full inventory')
  t.alike(focused.inventory.missingNames, [])

  const duplicate = verifyProfileVectors([accounting, { ...accounting }])
  t.absent(duplicate.valid, 'duplicate vector names fail even outside full inventory mode')
  t.ok(duplicate.inventory.errors.includes('duplicate profile vector name: accounting-receipt-v1-os-grounded'))
})

test('profile vector verifier rejects tampered accounting and unknown vectors', async (t) => {
  const vectors = await loadVectors()
  const accounting = vectors.find(vector => vector.name === 'accounting-receipt-v1-os-grounded')
  const tampered = {
    ...accounting,
    receipt: {
      ...accounting.receipt,
      diskBytes: accounting.receipt.diskBytes + 1
    }
  }
  const tamperedResult = verifyProfileVector(tampered)
  t.absent(tamperedResult.valid, 'tampered receipt vector fails')
  t.ok(tamperedResult.errors.some(error => error.includes('failed verification') || error.includes('signableHex')))

  const routeMatrix = vectors.find(vector => vector.name === 'relaykernel-http-route-matrix-v1-blindspark-compat')
  const missingSurface = {
    ...routeMatrix,
    surfaces: routeMatrix.surfaces.filter(surface => surface !== '/catalog.json')
  }
  const routeMatrixResult = verifyProfileVector(missingSurface)
  t.absent(routeMatrixResult.valid, 'tampered route matrix vector fails')
  t.ok(routeMatrixResult.errors.includes('relaykernel http route surfaces mismatch'))

  const rangeWithoutHead = {
    ...routeMatrix,
    matrix: routeMatrix.matrix.map(row => row.surface === '/v1/hyper/:driveKey/*path'
      ? {
          ...row,
          methods: row.methods.filter(method => method !== 'HEAD')
        }
      : row)
  }
  const rangeWithoutHeadResult = verifyProfileVector(rangeWithoutHead)
  t.absent(rangeWithoutHeadResult.valid, 'Range-capable route matrix row without HEAD fails')
  t.ok(rangeWithoutHeadResult.errors.includes(
    'relaykernel route matrix Range surface missing HEAD: /v1/hyper/:driveKey/*path'
  ))

  const appModuleBoundary = vectors.find(vector => vector.name === 'relaykernel-profile-v1-app-module-boundary')
  const hiddenAppModules = {
    ...appModuleBoundary,
    profile: {
      ...appModuleBoundary.profile,
      security: {
        ...appModuleBoundary.profile.security,
        appModulesExcludedFromKernel: true
      },
      appModules: []
    },
    verdict: {
      ...appModuleBoundary.verdict,
      warnings: []
    }
  }
  const appModuleBoundaryResult = verifyProfileVector(hiddenAppModules)
  t.absent(appModuleBoundaryResult.valid, 'tampered app-module boundary vector fails')
  t.ok(appModuleBoundaryResult.errors.includes('relaykernel profile mismatch'))
  t.ok(appModuleBoundaryResult.errors.includes('relaykernel verdict mismatch'))

  const unknown = verifyProfileVector({ name: 'future-vector' })
  t.absent(unknown.valid, 'unknown vector fails closed')
  t.ok(unknown.errors.includes('unsupported vector: future-vector'))

  const malformed = verifyProfileVector({
    name: 'retrievability-proof-signable-v1',
    seedHex: 'not-hex',
    coreKey: 'not-hex',
    nonce: 'not-hex',
    blockValueHex: 'not-hex'
  })
  t.absent(malformed.valid, 'malformed vector fails closed')
  t.ok(malformed.errors.some(error => error.startsWith('vector verification error:')))
})

test('profile vector CLI verifies fixture directory', async (t) => {
  const result = await runScript()
  t.is(result.code, 0)
  t.ok(result.stdout.includes('profile vectors:'))
  t.ok(result.stdout.includes('passed'))
  t.is(result.stderr, '')
})

test('profile vector CLI emits JSON summary', async (t) => {
  const result = await runScript(['--json'])
  t.is(result.code, 0)
  const summary = JSON.parse(result.stdout)
  t.ok(summary.valid)
  t.is(summary.failed, 0)
})

test('profile vector CLI fails on a tampered fixture file', async (t) => {
  // Portable, isolated temp dir with awaited cleanup. The previous hardcoded
  // '/private/tmp' path is a macOS-only symlink; on the Linux CI runner that
  // directory does not exist, so the write rejected ENOENT and surfaced as an
  // "async activity after the test ended" unhandledRejection that failed the
  // whole unit-tests job even though every assertion passed.
  const dir = await mkdtemp(path.join(tmpdir(), 'tampered-profile-vector-'))
  t.teardown(() => rm(dir, { recursive: true, force: true }))
  const file = path.join(dir, 'tampered.json')
  const vectors = await loadVectors()
  const proof = vectors.find(vector => vector.name === 'retrievability-proof-signable-v1')
  const tampered = { ...proof, signature: '00'.repeat(64) }

  await writeFile(file, JSON.stringify(tampered), 'utf8')
  const result = await runScript([file])
  t.is(result.code, 1)
  t.ok(result.stderr.includes('retrievability-proof-signable-v1'))
})