OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown commits / 745ddce

test: the 36-vector conformance harness for strip()
Red for the right reason - lib/strip.js does not exist yet. Asserts on BYTES rather than strings, because strip 8 exists to catch character-offset implementations that only break for non-ASCII authors, and 5.5 forbids the Unicode normalisation that would make string comparison safe. Also covers totality (2) and the facet invariants (6) as properties, which the vectors cannot express. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
745ddcebd37d20e5e03da72ff95fa2cd431a8684
by plankatron <fleeky@gmail.com> · 2026-09-05T22:42:12.000Z

Changed files (1) +78 -0

test/strip.test.js +78 -0
……@@ -0,0 +1,78 @@
1// The conformance harness for markdown-strip.md.
2//
3// The 36 vectors ARE the specification's oracle: "an implementation is conformant when it
4// reproduces EVERY vector byte-for-byte" (strip §8). So this file asserts on bytes, never on
5// shapes — a test that accepted "close enough" would certify exactly the class of defect §8
6// exists to catch (an implementation using character offsets instead of UTF-8 byte offsets,
7// which is invisible until a non-ASCII author posts).
8//
9// Facets in the vector file are the compact form { byteStart, byteEnd, type, value }; strip()
10// returns app.bsky.richtext.facet shape. expandFacet() below is the ONLY translation, and it is
11// deliberately in the test rather than in lib/ so that lib/ owns one representation.
12
13import test from 'brittle'
14import { readFileSync } from 'node:fs'
15import { strip } from '../lib/strip.js'
16
17const VECTORS = JSON.parse(
18 readFileSync(new URL('../spec/vectors/markdown-strip.json', import.meta.url), 'utf8')
19).vectors
20
21// Compact vector facet -> the app.bsky.richtext.facet shape strip() emits.
22// strip §6.5: features arrays contain EXACTLY ONE feature; we never emit a multi-feature facet.
23function expandFacet (f) {
24 const feature =
25 f.type === 'link' ? { $type: 'app.bsky.richtext.facet#link', uri: f.value }
26 : f.type === 'mention' ? { $type: 'app.bsky.richtext.facet#mention', did: f.value }
27 : f.type === 'tag' ? { $type: 'app.bsky.richtext.facet#tag', tag: f.value }
28 : null
29 if (!feature) throw new Error('vector uses an unknown facet type: ' + f.type)
30 return { index: { byteStart: f.byteStart, byteEnd: f.byteEnd }, features: [feature] }
31}
32
33const utf8 = (s) => Buffer.from(s, 'utf8')
34
35test('strip: all 36 vectors reproduce byte-for-byte', (t) => {
36 t.is(VECTORS.length, 36, 'the vector set is the one the spec pins')
37
38 for (const v of VECTORS) {
39 const got = strip(v.source)
40
41 // Bytes, not strings: two different strings can compare equal after normalisation, and
42 // strip §5.5 forbids Unicode normalisation precisely because it would change the CID.
43 t.alike(utf8(got.text), utf8(v.text), v.name + ': text — ' + v.why)
44 t.alike(got.facets, v.facets.map(expandFacet), v.name + ': facets — ' + v.why)
45 }
46})
47
48test('strip: is total — never throws (strip §2)', (t) => {
49 const hostile = [
50 '', ' ', ' ', '[', '[](', '![', '```', ':::', '~~~', '|',
51 '[[', '[^', '<https://', '#'.repeat(100), '\r\n\r\n',
52 String.fromCharCode(0xd800) // a lone surrogate: not valid UTF-8, must not throw
53 ]
54 for (const s of hostile) {
55 try {
56 const r = strip(s)
57 t.ok(typeof r.text === 'string' && Array.isArray(r.facets), 'total on ' + JSON.stringify(s))
58 } catch (err) {
59 t.fail('threw on ' + JSON.stringify(s) + ': ' + err.message)
60 }
61 }
62})
63
64test('strip: facet invariants hold on every vector (strip §6)', (t) => {
65 for (const v of VECTORS) {
66 const { text, facets } = strip(v.source)
67 const bytes = utf8(text).length
68 let prev = -1
69 for (const f of facets) {
70 const { byteStart, byteEnd } = f.index
71 t.ok(byteStart >= 0 && byteEnd <= bytes, v.name + ': facet within text bounds')
72 t.ok(byteEnd > byteStart, v.name + ': facet is non-empty (§6.3 drops empty ranges)')
73 t.ok(byteStart >= prev, v.name + ': facets sorted by byteStart (§6.1)')
74 t.is(f.features.length, 1, v.name + ': exactly one feature per facet (§6.5)')
75 prev = byteEnd
76 }
77 }
78})