/**
* 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), '')
})