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