OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown / test / strip.test.js

82 lines · javascript ref:
// The conformance harness for markdown-strip.md.
//
// The 36 vectors ARE the specification's oracle: "an implementation is conformant when it
// reproduces EVERY vector byte-for-byte" (strip §8). So this file asserts on bytes, never on
// shapes — a test that accepted "close enough" would certify exactly the class of defect §8
// exists to catch (an implementation using character offsets instead of UTF-8 byte offsets,
// which is invisible until a non-ASCII author posts).
//
// Facets in the vector file are the NATIVE form { byteStart, byteEnd, type, value } and strip()
// emits exactly that, so there is no translation step here at all. There used to be one:
// expandFacet() rebuilt each vector into app.bsky.richtext.facet shape because that is what the
// codec emitted. That shape left the core on 2026-09-06 (MVP.md §4 decision 3) — a bsky facet is
// now built by project(rec,'bsky'), the one place allowed to know atproto's names.

import test from 'brittle'
import { readFileSync } from '#node:fs'
import { strip } from '../lib/strip.js'

const VECTORS = JSON.parse(
  readFileSync(new URL('../spec/vectors/markdown-strip.json', import.meta.url), 'utf8')
).vectors

const utf8 = (s) => Buffer.from(s, 'utf8')

test('strip: every vector reproduces byte-for-byte', (t) => {
  t.ok(VECTORS.length >= 36, 'the pinned vector set may grow, never shrink')

  for (const v of VECTORS) {
    const got = strip(v.source)

    // Bytes, not strings: two different strings can compare equal after normalisation, and
    // strip §5.5 forbids Unicode normalisation precisely because it would change the CID.
    t.alike(utf8(got.text), utf8(v.text), v.name + ': text — ' + v.why)
    t.alike(got.facets, v.facets, v.name + ': facets — ' + v.why)
  }
})

test('strip: is total — never throws (strip §2)', (t) => {
  const hostile = [
    '', '   ', ' ', '[', '[](', '![', '```', ':::', '~~~', '|',
    '[[', '[^', '<https://', '#'.repeat(100), '\r\n\r\n',
    String.fromCharCode(0xd800) // a lone surrogate: not valid UTF-8, must not throw
  ]
  for (const s of hostile) {
    try {
      const r = strip(s)
      t.ok(typeof r.text === 'string' && Array.isArray(r.facets), 'total on ' + JSON.stringify(s))
    } catch (err) {
      t.fail('threw on ' + JSON.stringify(s) + ': ' + err.message)
    }
  }
})

test('strip: facet invariants hold on every vector (strip §6)', (t) => {
  for (const v of VECTORS) {
    const { text, facets } = strip(v.source)
    const bytes = utf8(text).length
    let prev = -1
    for (const f of facets) {
      const { byteStart, byteEnd } = f
      t.ok(byteStart >= 0 && byteEnd <= bytes, v.name + ': facet within text bounds')
      t.ok(byteEnd > byteStart, v.name + ': facet is non-empty (§6.3 drops empty ranges)')
      t.ok(byteStart >= prev, v.name + ': facets sorted by byteStart (§6.1)')
      t.alike(Object.keys(f).sort(), ['byteEnd', 'byteStart', 'type', 'value'],
        v.name + ': the native facet has exactly four keys (§6.5 — one feature, so no array)')
      prev = byteEnd
    }
  }
})

// The gate for MVP.md §4 decision 3. It is not a style check: a facet shape is what every future
// consumer gets written against, so a single leaked $type here is the leak that "leaks forever".
// Asserted over the SERIALIZED output rather than over field names, because that is the only form
// that catches a bsky string smuggled in as a value.
test('strip: no atproto vocabulary reaches the core output (MVP §4 decision 3)', (t) => {
  for (const v of VECTORS) {
    const blob = JSON.stringify(strip(v.source))
    t.absent(/\$type|app\.bsky|richtext|\bfeatures\b|"did"/.test(blob),
      v.name + ': strip() output carries no atproto names')
  }
})