OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown / test / lint.test.js

112 lines · javascript ref:
// lint — library.md §9. "Every projection defect so far has been diagnosed by reading daemon logs.
// This makes it a one-liner against a file, offline."
//
// The lint LOGIC lives in lib/ and the CLI is a thin argv shell over it, so the thing worth testing
// is testable and the portable half stays free of Node builtins (§3).
//
// Severity matters and is asserted: an `error` means the record cannot be published (§6.2 fails
// closed), a `warn` means it publishes but loses something, and `info` is a fact worth surfacing.
// Conflating them is how "it lints clean" comes to mean nothing.

import test from 'brittle'
import { lint } from '../lib/lint.js'

const codes = (r) => r.issues.map((i) => i.code).sort()
const bySeverity = (r, s) => r.issues.filter((i) => i.severity === s).map((i) => i.code)

const GOOD = `---
{
  "createdAt": "2026-08-05T13:38:23.294Z",
  "langs": [
    "en"
  ]
}
---

hello **world** #p2p
`

test('a well-formed record lints clean', (t) => {
  const r = lint(GOOD)
  t.ok(r.ok, 'ok')
  t.alike(bySeverity(r, 'error'), [], 'no errors')
})

test('⛔ malformed frontmatter is an ERROR and blocks publication (§6.2)', (t) => {
  const r = lint('---\n{ "createdAt": oops }\n---\n\nbody\n')
  t.absent(r.ok)
  t.ok(bySeverity(r, 'error').includes('frontmatter-invalid-json'))
  t.ok(r.publishable === false, 'and the record is not publishable')
})

test('⛔ a missing createdAt is an ERROR — nothing may be published without it', (t) => {
  const r = lint('---\n{}\n---\n\nbody\n')
  t.absent(r.ok)
  t.ok(bySeverity(r, 'error').includes('no-created-at'))
})

test('an unknown network in reach is an ERROR, not a silent drop', (t) => {
  const r = lint('---\n{\n  "createdAt": "T",\n  "reach": ["bsky", "myspace"]\n}\n---\n\nbody\n')
  t.ok(bySeverity(r, 'error').includes('unknown-network'), 'names the network rather than ignoring it')
})

test('reach is reported against the room ceiling only when the room is KNOWN', (t) => {
  const src = '---\n{\n  "createdAt": "T",\n  "reach": ["bsky"]\n}\n---\n\nbody\n'
  t.alike(bySeverity(lint(src), 'error'), [], 'without a room, no claim is made — the CLI cannot know')

  const withRoom = lint(src, { roomType: 'followers' })
  t.ok(bySeverity(withRoom, 'error').includes('reach-exceeds-room'),
    '⛔ a followers room can never reach bsky, and saying so is the whole point')
})

test('a spoiler is a WARN — it arrives un-spoilered on bsky (§5, the thesis in one example)', (t) => {
  const r = lint('---\n{\n  "createdAt": "T"\n}\n---\n\n:::spoiler\nthe ending\n:::\n')
  t.ok(bySeverity(r, 'warn').includes('spoiler-revealed-on-bsky'),
    'lossless to the fediverse, lossy to bsky, and nothing else tells the author')
  t.ok(r.ok, 'a warning does not block publication')
})

test('an unknown directive name is INFO — it still yields its contents (§4.8)', (t) => {
  const r = lint('---\n{\n  "createdAt": "T"\n}\n---\n\n:::wat\nbody\n:::\n')
  t.ok(bySeverity(r, 'info').includes('unknown-directive'))
  t.ok(r.ok)
})

test('a bare @mention is INFO — it resolves or is omitted, never guessed (§3.4)', (t) => {
  const r = lint('---\n{\n  "createdAt": "T"\n}\n---\n\nhi @alice\n')
  t.ok(bySeverity(r, 'info').includes('unresolved-mention'))
})

test('a non-http link target is a WARN — the label survives, the link does not (§3.2)', (t) => {
  const r = lint('---\n{\n  "createdAt": "T"\n}\n---\n\n[x](javascript:alert(1))\n')
  t.ok(bySeverity(r, 'warn').includes('link-refused'))
})

test('an empty body with no embed is INFO, not an error (§3.5)', (t) => {
  // "A post whose entire source is one image strips to empty text, which is valid."
  const r = lint('---\n{\n  "createdAt": "T"\n}\n---\n\n![a cat](favela://blob/abc)\n')
  t.ok(r.ok, 'an image-only post is valid')
  t.ok(bySeverity(r, 'info').includes('empty-text'))
})

test('lint is TOTAL — never throws', (t) => {
  for (const s of ['', '---', '---\n---', 'no frontmatter', '---\n[]\n---\n', null, undefined]) {
    try {
      const r = lint(s)
      t.ok(Array.isArray(r.issues) && typeof r.ok === 'boolean', 'total on ' + JSON.stringify(s))
    } catch (err) {
      t.fail('threw on ' + JSON.stringify(s) + ': ' + err.message)
    }
  }
})

test('every issue carries a code, a severity and a spec reference', (t) => {
  const r = lint('---\n{ bad }\n---\n\n:::wat\nx\n:::\n\nhi @bob\n')
  t.ok(r.issues.length > 0)
  for (const i of r.issues) {
    t.ok(typeof i.code === 'string' && i.code.length > 0, 'code')
    t.ok(['error', 'warn', 'info'].includes(i.severity), 'severity is one of three')
    t.ok(typeof i.spec === 'string' && i.spec.length > 0, i.code + ' cites the spec section it enforces')
  }
})