OverviewFilesCommitsIssuesDocsReleasesPeers

pgh / test / pgh-schema.test.js

193 lines · javascript ref:
/**
 * pgh-schema tests — P.10 Phase 0.5h
 */

import test from 'brittle'
import { validatePghSection, summarizePghSection, formatValidation } from '../lib/schema.js'

// ── Basics ───────────────────────────────────────────────────

test('validate — missing section is valid', (t) => {
  const r = validatePghSection(undefined)
  t.ok(r.valid)
  t.is(r.errors.length, 0)
  t.is(r.warnings.length, 0)
})

test('validate — null is valid', (t) => {
  t.ok(validatePghSection(null).valid)
})

test('validate — non-object is an error', (t) => {
  const r = validatePghSection('not-an-object')
  t.absent(r.valid)
  t.ok(r.errors.length > 0)
})

test('validate — array is an error', (t) => {
  const r = validatePghSection([])
  t.absent(r.valid)
})

test('validate — unknown field produces a warning (but still valid)', (t) => {
  const r = validatePghSection({ mysteryKey: 'x' })
  t.ok(r.valid)
  t.is(r.warnings.length, 1)
  t.ok(r.warnings[0].includes('mysteryKey'))
  // Unknown field is preserved in normalized
  t.is(r.normalized.mysteryKey, 'x')
})

// ── bin ──────────────────────────────────────────────────────

test('bin — valid string', (t) => {
  const r = validatePghSection({ bin: 'pear-git' })
  t.ok(r.valid)
})

test('bin — empty is an error', (t) => {
  const r = validatePghSection({ bin: '' })
  t.absent(r.valid)
})

test('bin — with slash is an error', (t) => {
  const r = validatePghSection({ bin: 'path/to/bin' })
  t.absent(r.valid)
  t.ok(r.errors[0].includes('slashes'))
})

test('bin — with whitespace is an error', (t) => {
  const r = validatePghSection({ bin: 'pear git' })
  t.absent(r.valid)
})

// ── entrypoint ───────────────────────────────────────────────

test('entrypoint — valid relative path', (t) => {
  t.ok(validatePghSection({ entrypoint: 'index.js' }).valid)
  t.ok(validatePghSection({ entrypoint: 'src/main.js' }).valid)
})

test('entrypoint — absolute path is an error', (t) => {
  const r = validatePghSection({ entrypoint: '/abs/path.js' })
  t.absent(r.valid)
})

test('entrypoint — contains .. is an error', (t) => {
  t.absent(validatePghSection({ entrypoint: '../outside.js' }).valid)
})

// ── upgrade ──────────────────────────────────────────────────

test('upgrade — pgh:// and punch:// accepted', (t) => {
  t.ok(validatePghSection({ upgrade: 'pgh://fleeky/pear-git-v3' }).valid)
  t.ok(validatePghSection({ upgrade: 'punch://abc123/repo' }).valid)
})

test('upgrade — http:// is an error', (t) => {
  const r = validatePghSection({ upgrade: 'http://example.com' })
  t.absent(r.valid)
})

test('upgrade — non-string is an error', (t) => {
  t.absent(validatePghSection({ upgrade: 42 }).valid)
})

// ── service ──────────────────────────────────────────────────

test('service — valid config', (t) => {
  t.ok(validatePghSection({
    service: { port: 4040, prefix: '/git', host: '127.0.0.1', requiresNetwork: true }
  }).valid)
})

test('service.port — out of range is an error', (t) => {
  t.absent(validatePghSection({ service: { port: 0 } }).valid)
  t.absent(validatePghSection({ service: { port: 70000 } }).valid)
  t.absent(validatePghSection({ service: { port: 3.14 } }).valid)
})

test('service.prefix — must start with /', (t) => {
  t.absent(validatePghSection({ service: { prefix: 'git' } }).valid)
  t.ok(validatePghSection({ service: { prefix: '/git' } }).valid)
})

test('service.requiresNetwork — non-boolean is an error', (t) => {
  t.absent(validatePghSection({ service: { requiresNetwork: 'yes' } }).valid)
})

test('service — unknown subfield warns', (t) => {
  const r = validatePghSection({ service: { port: 4040, mystery: 'x' } })
  t.ok(r.valid)
  t.is(r.warnings.length, 1)
})

// ── systemd ──────────────────────────────────────────────────

test('systemd — valid config', (t) => {
  t.ok(validatePghSection({
    systemd: { user: true, enable: true, restart: 'on-failure' }
  }).valid)
})

test('systemd.restart — only allowed values', (t) => {
  t.ok(validatePghSection({ systemd: { restart: 'always' } }).valid)
  t.ok(validatePghSection({ systemd: { restart: 'no' } }).valid)
  t.absent(validatePghSection({ systemd: { restart: 'sometimes' } }).valid)
})

test('systemd.user — non-boolean is an error', (t) => {
  t.absent(validatePghSection({ systemd: { user: 'yes' } }).valid)
})

// ── type ─────────────────────────────────────────────────────

test('type — allowed values', (t) => {
  t.ok(validatePghSection({ type: 'terminal' }).valid)
  t.ok(validatePghSection({ type: 'desktop' }).valid)
  t.ok(validatePghSection({ type: 'daemon' }).valid)
  t.absent(validatePghSection({ type: 'other' }).valid)
})

// ── stage ────────────────────────────────────────────────────

test('stage.ignore — must be array', (t) => {
  t.ok(validatePghSection({ stage: { ignore: ['test/', '*.md'] } }).valid)
  t.absent(validatePghSection({ stage: { ignore: 'test/' } }).valid)
})

// ── summary + formatter ─────────────────────────────────────

test('summarizePghSection — produces readable one-per-line output', (t) => {
  const s = summarizePghSection({
    bin: 'pear-git',
    entrypoint: 'index.js',
    upgrade: 'pgh://fleeky/pear-git-v3',
    service: { port: 4040, prefix: '/git' },
    type: 'terminal'
  })
  t.ok(s.includes('bin: pear-git'))
  t.ok(s.includes('entrypoint: index.js'))
  t.ok(s.includes('upgrade: pgh://fleeky/pear-git-v3'))
  t.ok(s.includes('port=4040'))
  t.ok(s.includes('prefix=/git'))
})

test('summarizePghSection — undefined returns "(no pgh section)"', (t) => {
  t.is(summarizePghSection(undefined), '(no pgh section)')
})

test('formatValidation — error output is loud and numbered', (t) => {
  const r = validatePghSection({ bin: '', upgrade: 'http://no.way' })
  const out = formatValidation(r, 'test-pkg.json')
  t.ok(out.includes('ERROR: test-pkg.json'))
  t.ok(out.includes('2 errors'))
  t.ok(out.includes('pgh.bin'))
  t.ok(out.includes('pgh.upgrade'))
})

test('formatValidation — clean input returns empty string', (t) => {
  const r = validatePghSection({ bin: 'good', type: 'terminal' })
  t.is(formatValidation(r), '')
})