Files
deepseek-harness/examples/desktop/test/md-mini.test.js

304 lines
10 KiB
JavaScript

// Tests for the minimal Markdown → DOM renderer (src/renderer/md-mini.js).
// Two layers: (1) pure parse functions (parseBlocks / parseInline) exercised
// directly, and (2) the DOM builder run against a hand-rolled fake `document`
// so we can assert node types / textContent WITHOUT a browser — the whole
// security claim is "every model char reaches the DOM as a text node", and the
// fake document makes that observable.
'use strict'
const { test } = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const md = require('../src/renderer/md-mini.js')
// ---- fake DOM ------------------------------------------------------------
// Minimal enough for md-mini: createElement/createTextNode, appendChild,
// textContent (get walks children), className, setAttribute, addEventListener.
function makeNode(tag) {
const node = {
tagName: tag ? tag.toUpperCase() : undefined,
nodeType: tag ? 1 : 3,
children: [],
childNodes: [],
attrs: {},
listeners: {},
className: '',
_text: '',
appendChild(child) {
this.childNodes.push(child)
if (child.nodeType === 1) this.children.push(child)
child.parentNode = this
return child
},
setAttribute(k, v) {
this.attrs[k] = v
},
getAttribute(k) {
return this.attrs[k]
},
addEventListener(ev, fn) {
;(this.listeners[ev] = this.listeners[ev] || []).push(fn)
},
set textContent(v) {
this._text = String(v)
this.childNodes = []
this.children = []
},
get textContent() {
if (this.nodeType === 3) return this._text
if (this.childNodes.length === 0) return this._text
return this.childNodes.map((c) => c.textContent).join('')
},
}
return node
}
function fakeDoc() {
return {
createElement: (tag) => makeNode(tag),
createTextNode: (t) => {
const n = makeNode(null)
n._text = String(t)
return n
},
}
}
function render(src, onLink) {
return md.render(src, { document: fakeDoc(), onLink })
}
// walk helper: collect all descendant element nodes with a given tag
function findAll(root, tag) {
const out = []
const want = tag.toUpperCase()
const walk = (n) => {
for (const c of n.childNodes) {
if (c.nodeType === 1) {
if (c.tagName === want) out.push(c)
walk(c)
}
}
}
walk(root)
return out
}
// ---- parseBlocks ---------------------------------------------------------
test('md-mini: headings h1..h6 parse with level and text', () => {
const { blocks } = md.parseBlocks('# One\n## Two\n###### Six')
assert.deepEqual(
blocks.map((b) => [b.type, b.level, b.text]),
[
['heading', 1, 'One'],
['heading', 2, 'Two'],
['heading', 6, 'Six'],
],
)
})
test('md-mini: 7 hashes is not a heading (paragraph)', () => {
const { blocks } = md.parseBlocks('####### nope')
assert.equal(blocks[0].type, 'paragraph')
})
test('md-mini: fenced code captured verbatim, no inline parse', () => {
const { blocks } = md.parseBlocks('```js\nconst x = **not bold**\n```')
assert.equal(blocks.length, 1)
assert.equal(blocks[0].type, 'code')
assert.equal(blocks[0].lang, 'js')
assert.equal(blocks[0].text, 'const x = **not bold**')
})
test('md-mini: tilde fence closes only on tildes', () => {
const { blocks } = md.parseBlocks('~~~\n```\nstill code\n~~~')
assert.equal(blocks.length, 1)
assert.equal(blocks[0].type, 'code')
assert.equal(blocks[0].text, '```\nstill code')
})
test('md-mini: unordered list groups consecutive items', () => {
const { blocks } = md.parseBlocks('- a\n- b\n* c')
assert.equal(blocks.length, 1)
assert.equal(blocks[0].type, 'list')
assert.equal(blocks[0].ordered, false)
assert.deepEqual(blocks[0].items, ['a', 'b', 'c'])
})
test('md-mini: ordered and unordered lists split into separate blocks', () => {
const { blocks } = md.parseBlocks('1. a\n2. b\n- c')
assert.equal(blocks.length, 2)
assert.equal(blocks[0].ordered, true)
assert.deepEqual(blocks[0].items, ['a', 'b'])
assert.equal(blocks[1].ordered, false)
assert.deepEqual(blocks[1].items, ['c'])
})
test('md-mini: blockquote merges consecutive lines', () => {
const { blocks } = md.parseBlocks('> line one\n> line two')
assert.equal(blocks[0].type, 'quote')
assert.equal(blocks[0].text, 'line one line two')
})
test('md-mini: horizontal rule vs list disambiguation', () => {
const hr = md.parseBlocks('---')
assert.equal(hr.blocks[0].type, 'hr')
const list = md.parseBlocks('- item')
assert.equal(list.blocks[0].type, 'list')
})
test('md-mini: paragraph joins soft-wrapped lines', () => {
const { blocks } = md.parseBlocks('hello\nworld\n\nsecond')
assert.equal(blocks.length, 2)
assert.equal(blocks[0].text, 'hello world')
assert.equal(blocks[1].text, 'second')
})
test('md-mini: blank input yields no blocks, not truncated', () => {
const { blocks, truncated } = md.parseBlocks('')
assert.deepEqual(blocks, [])
assert.equal(truncated, false)
})
test('md-mini: length cap at MAX_LINES sets truncated', () => {
const many = Array.from({ length: md.MAX_LINES + 50 }, (_, i) => 'line ' + i).join('\n')
const { blocks, truncated } = md.parseBlocks(many)
assert.equal(truncated, true)
// Only the first MAX_LINES lines fed the parser; they collapse into one
// paragraph (soft-wrapped), so assert the last surviving line made it and
// the first dropped one did not.
const text = blocks.map((b) => b.text || '').join(' ')
assert.ok(text.includes('line ' + (md.MAX_LINES - 1)))
assert.ok(!text.includes('line ' + md.MAX_LINES))
})
// ---- parseInline ---------------------------------------------------------
test('md-mini: inline code is verbatim and beats other markup', () => {
const toks = md.parseInline('a `**b**` c')
assert.deepEqual(
toks.map((t) => [t.type, t.text]),
[
['text', 'a '],
['code', '**b**'],
['text', ' c'],
],
)
})
test('md-mini: strong and emphasis', () => {
assert.equal(md.parseInline('**x**')[0].type, 'strong')
assert.equal(md.parseInline('__x__')[0].type, 'strong')
assert.equal(md.parseInline('*x*')[0].type, 'em')
assert.equal(md.parseInline('_x_')[0].type, 'em')
})
test('md-mini: link token carries text and href', () => {
const toks = md.parseInline('see [docs](https://x.test/p)')
const link = toks.find((t) => t.type === 'link')
assert.equal(link.text, 'docs')
assert.equal(link.href, 'https://x.test/p')
})
test('md-mini: lone asterisk is literal text', () => {
const toks = md.parseInline('2 * 3 = 6')
assert.equal(toks.length, 1)
assert.equal(toks[0].type, 'text')
assert.equal(toks[0].text, '2 * 3 = 6')
})
test('md-mini: isSafeHref whitelist', () => {
assert.equal(md.isSafeHref('https://x.test'), true)
assert.equal(md.isSafeHref('http://x.test'), true)
assert.equal(md.isSafeHref('mailto:a@b.test'), true)
assert.equal(md.isSafeHref('javascript:alert(1)'), false)
assert.equal(md.isSafeHref('data:text/html,<script>'), false)
assert.equal(md.isSafeHref('file:///etc/passwd'), false)
})
// ---- DOM build + security ------------------------------------------------
test('md-mini: render produces expected element tags', () => {
const root = render('# Title\n\npara\n\n- a\n- b\n\n> quote\n\n```\ncode\n```')
assert.equal(findAll(root, 'h1').length, 1)
assert.equal(findAll(root, 'p').length, 1)
assert.equal(findAll(root, 'ul').length, 1)
assert.equal(findAll(root, 'li').length, 2)
assert.equal(findAll(root, 'blockquote').length, 1)
assert.equal(findAll(root, 'pre').length, 1)
})
test('md-mini: raw HTML in markdown stays literal text (no nodes)', () => {
const evil = 'before <script>alert(1)</script> <img src=x onerror=alert(2)> after'
const root = render(evil)
// No <script> or <img> element was ever created.
assert.equal(findAll(root, 'script').length, 0)
assert.equal(findAll(root, 'img').length, 0)
// The angle-bracket text survives verbatim in the paragraph textContent.
assert.ok(root.textContent.includes('<script>alert(1)</script>'))
assert.ok(root.textContent.includes('<img src=x onerror=alert(2)>'))
})
test('md-mini: HTML inside fenced code stays literal', () => {
const root = render('```\n<script>evil()</script>\n```')
assert.equal(findAll(root, 'script').length, 0)
const pre = findAll(root, 'pre')[0]
assert.equal(pre.textContent, '<script>evil()</script>')
})
test('md-mini: safe link builds <a> with click routed to onLink, default prevented', () => {
const opened = []
const root = render('[go](https://x.test/p)', (href) => opened.push(href))
const a = findAll(root, 'a')[0]
assert.ok(a)
assert.equal(a.getAttribute('href'), 'https://x.test/p')
assert.equal(a.getAttribute('rel'), 'noreferrer noopener')
let prevented = false
a.listeners.click[0]({ preventDefault: () => (prevented = true) })
assert.equal(prevented, true)
assert.deepEqual(opened, ['https://x.test/p'])
})
test('md-mini: unsafe link scheme renders inert text, no <a>', () => {
const opened = []
const root = render('[click](javascript:alert(1))', (href) => opened.push(href))
assert.equal(findAll(root, 'a').length, 0)
assert.ok(root.textContent.includes('click'))
assert.deepEqual(opened, [])
})
test('md-mini: nested emphasis inside strong', () => {
const root = render('**bold _and italic_**')
const strong = findAll(root, 'strong')[0]
assert.ok(strong)
assert.equal(findAll(strong, 'em').length, 1)
})
test('md-mini: truncated note appended past cap', () => {
const many = Array.from({ length: md.MAX_LINES + 10 }, () => 'x').join('\n')
const root = render(many)
const note = root.childNodes.find(
(c) => c.nodeType === 1 && c.className === 'md-mini-truncated',
)
assert.ok(note, 'expected a truncated note element')
assert.ok(note.textContent.includes(String(md.MAX_LINES)))
})
// ---- registration --------------------------------------------------------
test('md-mini: registered as a script in index.html before artifacts.js', () => {
const html = fs.readFileSync(
path.join(__dirname, '..', 'src', 'renderer', 'index.html'),
'utf8',
)
const mdIdx = html.indexOf('"./md-mini.js"')
const artIdx = html.indexOf('"./artifacts.js"')
assert.ok(mdIdx > -1, 'md-mini.js must be script-registered')
assert.ok(mdIdx < artIdx, 'md-mini.js must load before artifacts.js')
})