mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
392 lines
15 KiB
JavaScript
392 lines
15 KiB
JavaScript
// Tests for the in-stream inline artifact preview (lane-artifact-inline).
|
|
//
|
|
// Two surfaces:
|
|
// (a) Behavior — load src/renderer/artifacts.js against a handrolled DOM
|
|
// stub (same approach as lane-ctx-deep-dom.test.js), drive the real
|
|
// onArtifactEvent path, then click the preview toggle and assert the
|
|
// region expands and builds the right content (md render / html
|
|
// iframe / server-down fallback). The real md-mini module is wired in
|
|
// as window.__dshMdMini so the md branch renders genuine DOM.
|
|
// (b) Source + CSS locks — the iframe sandbox value (no allow-same-origin)
|
|
// and the preview stylesheet block, so security-relevant drift trips
|
|
// a gate even if the behavior stub is loosened.
|
|
|
|
'use strict'
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
|
|
const ROOT = path.join(__dirname, '..')
|
|
|
|
// ---- DOM stub ------------------------------------------------------------
|
|
// Covers exactly what artifacts.js touches: createElement/createTextNode,
|
|
// append/appendChild, className/classList, dataset, hidden, open, textContent,
|
|
// innerHTML (stored, never parsed), setAttribute/getAttribute,
|
|
// addEventListener + a dispatch helper, and querySelector/querySelectorAll /
|
|
// closest for the specific selectors the module uses.
|
|
|
|
function makeEl(tag, doc) {
|
|
const el = {
|
|
tagName: tag ? String(tag).toUpperCase() : undefined,
|
|
nodeType: tag ? 1 : 3,
|
|
ownerDocument: doc,
|
|
className: '',
|
|
_text: '',
|
|
innerHTML: '',
|
|
hidden: false,
|
|
open: false,
|
|
src: '',
|
|
href: '',
|
|
title: '',
|
|
type: '',
|
|
disabled: false,
|
|
dataset: {},
|
|
_attrs: {},
|
|
_listeners: {},
|
|
_children: [],
|
|
parentNode: null,
|
|
scrollTop: 0,
|
|
scrollHeight: 0,
|
|
offsetWidth: 0,
|
|
classList: {
|
|
_set: new Set(),
|
|
add(c) { this._set.add(c) },
|
|
remove(c) { this._set.delete(c) },
|
|
contains(c) { return this._set.has(c) },
|
|
},
|
|
appendChild(c) { c.parentNode = el; el._children.push(c); return c },
|
|
append(...kids) { for (const k of kids) { k.parentNode = el; el._children.push(k) } },
|
|
removeChild(c) {
|
|
const i = el._children.indexOf(c)
|
|
if (i >= 0) el._children.splice(i, 1)
|
|
return c
|
|
},
|
|
remove() { if (el.parentNode) el.parentNode.removeChild(el) },
|
|
replaceWith(next) {
|
|
if (!el.parentNode) return
|
|
const i = el.parentNode._children.indexOf(el)
|
|
if (i >= 0) el.parentNode._children[i] = next
|
|
next.parentNode = el.parentNode
|
|
},
|
|
setAttribute(k, v) { el._attrs[k] = String(v) },
|
|
getAttribute(k) { return k in el._attrs ? el._attrs[k] : null },
|
|
removeAttribute(k) { delete el._attrs[k] },
|
|
addEventListener(t, fn) { (el._listeners[t] = el._listeners[t] || []).push(fn) },
|
|
dispatch(t, ev) { for (const fn of el._listeners[t] || []) fn(ev || {}) },
|
|
set textContent(v) { el._text = String(v); el._children = [] },
|
|
get textContent() {
|
|
if (el.nodeType === 3) return el._text
|
|
if (el._children.length === 0) return el._text
|
|
return el._children.map((c) => c.textContent).join('')
|
|
},
|
|
closest(sel) {
|
|
let n = el
|
|
while (n) {
|
|
if (matches(n, sel)) return n
|
|
n = n.parentNode
|
|
}
|
|
return null
|
|
},
|
|
querySelector(sel) { return queryAll(el, sel)[0] || null },
|
|
querySelectorAll(sel) { return queryAll(el, sel) },
|
|
}
|
|
return el
|
|
}
|
|
|
|
// Minimal selector matcher: 'tag', '.class', '[attr]', '[attr="v"]'.
|
|
function matches(el, sel) {
|
|
if (!el || el.nodeType !== 1) return false
|
|
sel = sel.trim()
|
|
if (sel.startsWith('.')) {
|
|
const cls = sel.slice(1)
|
|
return el.classList._set.has(cls) || String(el.className).split(/\s+/).includes(cls)
|
|
}
|
|
const attrEq = /^\[([\w-]+)="([^"]*)"\]$/.exec(sel)
|
|
if (attrEq) return (el.dataset[toCamel(attrEq[1])] ?? el._attrs[attrEq[1]]) === attrEq[2]
|
|
const attr = /^\[([\w-]+)\]$/.exec(sel)
|
|
if (attr) return el._attrs[attr[1]] != null || el.dataset[toCamel(attr[1])] != null
|
|
return el.tagName === sel.toUpperCase()
|
|
}
|
|
function toCamel(s) { return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()) }
|
|
|
|
// querySelectorAll supporting comma lists and ':scope > sel'. Descendant
|
|
// search otherwise (children recursively).
|
|
function queryAll(root, sel) {
|
|
const out = []
|
|
for (const part of sel.split(',').map((s) => s.trim())) {
|
|
if (part.startsWith(':scope >')) {
|
|
const child = part.slice(':scope >'.length).trim()
|
|
for (const c of root._children) if (matches(c, child)) out.push(c)
|
|
} else {
|
|
const walk = (n) => {
|
|
for (const c of n._children || []) {
|
|
if (matches(c, part)) out.push(c)
|
|
walk(c)
|
|
}
|
|
}
|
|
walk(root)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
function makeDoc() {
|
|
const doc = {
|
|
readyState: 'complete',
|
|
_byId: {},
|
|
createElement(tag) { return makeEl(tag, doc) },
|
|
createTextNode(t) { const n = makeEl(null, doc); n._text = String(t); return n },
|
|
getElementById(id) { return doc._byId[id] || null },
|
|
addEventListener() {},
|
|
}
|
|
return doc
|
|
}
|
|
|
|
function findAllByClass(root, cls, out) {
|
|
out = out || []
|
|
if (!root) return out
|
|
const has = (root.classList && root.classList._set.has(cls)) ||
|
|
String(root.className || '').split(/\s+/).includes(cls)
|
|
if (has) out.push(root)
|
|
for (const c of root._children || []) findAllByClass(c, cls, out)
|
|
return out
|
|
}
|
|
|
|
// ---- module loader -------------------------------------------------------
|
|
// Fresh module instance per test with the globals it reads. Returns the
|
|
// exposed __dshArtifacts API plus the stubbed doc/window/stream so tests can
|
|
// drive events and inspect the resulting tree.
|
|
|
|
function loadArtifacts(opts) {
|
|
opts = opts || {}
|
|
const doc = makeDoc()
|
|
const stream = doc.createElement('div')
|
|
doc._byId.stream = stream
|
|
|
|
const dsh = {
|
|
onArtifact() {},
|
|
openArtifact: async () => ({ ok: true }),
|
|
getArtifactBase: opts.getArtifactBase || (async () => ({ url: null, dir: '/tmp/a' })),
|
|
openExternalUrl: opts.openExternalUrl || (() => {}),
|
|
}
|
|
const win = { dsh }
|
|
// Real md-mini so the md branch renders genuine DOM.
|
|
const md = require('../src/renderer/md-mini.js')
|
|
win.__dshMdMini = md
|
|
|
|
const sandbox = {
|
|
window: win,
|
|
document: doc,
|
|
console,
|
|
setTimeout,
|
|
clearTimeout,
|
|
Promise,
|
|
Date,
|
|
module: { exports: {} },
|
|
}
|
|
win.window = win
|
|
|
|
const src = fs.readFileSync(path.join(ROOT, 'src/renderer/artifacts.js'), 'utf8')
|
|
const vm = require('node:vm')
|
|
vm.runInNewContext(src, sandbox)
|
|
|
|
return { api: win.__dshArtifacts, doc, win, stream, dsh }
|
|
}
|
|
|
|
// The md/html card lands inside the auto-built panel's `.artifact-group`.
|
|
function firstCard(stream) {
|
|
return findAllByClass(stream, 'artifact-card')[0]
|
|
}
|
|
function tick() { return new Promise((r) => setTimeout(r, 0)) }
|
|
|
|
// ---- behavior: collapsed by default -------------------------------------
|
|
|
|
test('inline preview: md card starts collapsed (region hidden, caret ▸)', () => {
|
|
const { api, stream } = loadArtifacts()
|
|
api.onArtifactEvent({ artifactId: 'notes.md', kind: 'md', version: 1, blob: '# Hi\n\nbody' })
|
|
const card = firstCard(stream)
|
|
assert.ok(card, 'expected an artifact card in the stream')
|
|
const region = findAllByClass(card, 'artifact-preview-region')[0]
|
|
const toggle = findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
assert.ok(region && toggle, 'expected preview toggle + region')
|
|
assert.equal(region.hidden, true)
|
|
assert.equal(toggle.getAttribute('aria-expanded'), 'false')
|
|
})
|
|
|
|
test('inline preview: non-md/html kind gets NO preview strip', () => {
|
|
const { api, stream } = loadArtifacts()
|
|
api.onArtifactEvent({ artifactId: 'chart.svg', kind: 'svg', version: 1, blob: '<svg/>' })
|
|
const card = firstCard(stream)
|
|
assert.equal(findAllByClass(card, 'artifact-preview').length, 0)
|
|
})
|
|
|
|
// ---- behavior: md expand renders real DOM -------------------------------
|
|
|
|
test('inline preview: expanding md builds rendered markdown, no raw HTML nodes', () => {
|
|
const { api, stream } = loadArtifacts()
|
|
api.onArtifactEvent({
|
|
artifactId: 'doc.md',
|
|
kind: 'md',
|
|
version: 1,
|
|
blob: '# Title\n\n**bold** and `code`\n\n<script>x</script>',
|
|
})
|
|
const card = firstCard(stream)
|
|
const toggle = findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
toggle.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
|
|
const region = findAllByClass(card, 'artifact-preview-region')[0]
|
|
assert.equal(region.hidden, false)
|
|
assert.equal(toggle.getAttribute('aria-expanded'), 'true')
|
|
const mdBlock = findAllByClass(card, 'artifact-preview-md')[0]
|
|
assert.ok(mdBlock, 'expected rendered markdown block')
|
|
assert.equal(findAllByClass(mdBlock, 'md-mini-h1').length, 1)
|
|
// The <script> stayed literal text — no script element was created.
|
|
const scripts = []
|
|
;(function walk(n) { for (const c of n._children || []) { if (c.tagName === 'SCRIPT') scripts.push(c); walk(c) } })(mdBlock)
|
|
assert.equal(scripts.length, 0)
|
|
assert.ok(mdBlock.textContent.includes('<script>x</script>'))
|
|
})
|
|
|
|
test('inline preview: md link routes through openExternalUrl', () => {
|
|
const opened = []
|
|
const { api, stream } = loadArtifacts({ openExternalUrl: (u) => opened.push(u) })
|
|
api.onArtifactEvent({
|
|
artifactId: 'links.md',
|
|
kind: 'md',
|
|
version: 1,
|
|
blob: 'see [site](https://x.test/p)',
|
|
})
|
|
const card = firstCard(stream)
|
|
findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
const link = findAllByClass(card, 'md-mini-link')[0]
|
|
assert.ok(link, 'expected a rendered md link')
|
|
link.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
assert.deepEqual(opened, ['https://x.test/p'])
|
|
})
|
|
|
|
test('inline preview: md with no blob shows honest "content not supplied" note', () => {
|
|
const { api, stream } = loadArtifacts()
|
|
// Real ArtifactServer path — event carries no blob.
|
|
api.onArtifactEvent({ artifactId: 'server.md', kind: 'md', version: 1, path: '/a/server.md' })
|
|
const card = firstCard(stream)
|
|
findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
assert.equal(findAllByClass(card, 'artifact-preview-md').length, 0)
|
|
const note = findAllByClass(card, 'artifact-preview-note')[0]
|
|
assert.ok(note, 'expected a fallback note')
|
|
})
|
|
|
|
test('inline preview: collapse toggles region back to hidden', () => {
|
|
const { api, stream } = loadArtifacts()
|
|
api.onArtifactEvent({ artifactId: 't.md', kind: 'md', version: 1, blob: '# x' })
|
|
const card = firstCard(stream)
|
|
const toggle = findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
const region = findAllByClass(card, 'artifact-preview-region')[0]
|
|
toggle.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
assert.equal(region.hidden, false)
|
|
toggle.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
assert.equal(region.hidden, true)
|
|
})
|
|
|
|
// ---- behavior: html iframe + fallback -----------------------------------
|
|
|
|
test('inline preview: html expand mounts sandboxed iframe when server URL present', async () => {
|
|
const { api, stream } = loadArtifacts()
|
|
api.onArtifactEvent({
|
|
artifactId: 'page.html',
|
|
kind: 'html',
|
|
version: 1,
|
|
url: 'http://127.0.0.1:9812/a/page.html/',
|
|
})
|
|
const card = firstCard(stream)
|
|
findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
await tick()
|
|
const frame = findAllByClass(card, 'artifact-preview-frame')[0]
|
|
assert.ok(frame, 'expected an iframe')
|
|
assert.equal(frame.tagName, 'IFRAME')
|
|
assert.equal(frame.getAttribute('sandbox'), 'allow-scripts')
|
|
assert.ok(!/allow-same-origin/.test(frame.getAttribute('sandbox')))
|
|
assert.equal(frame.src, 'http://127.0.0.1:9812/a/page.html/')
|
|
})
|
|
|
|
test('inline preview: html falls back to open-in-browser when server is down', async () => {
|
|
const { api, stream } = loadArtifacts({ getArtifactBase: async () => ({ url: null }) })
|
|
// No `url` on the event AND base.url null → server down.
|
|
api.onArtifactEvent({ artifactId: 'down.html', kind: 'html', version: 1, path: '/a/down.html' })
|
|
const card = firstCard(stream)
|
|
findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
await tick()
|
|
assert.equal(findAllByClass(card, 'artifact-preview-frame').length, 0)
|
|
const note = findAllByClass(card, 'artifact-preview-note')[0]
|
|
assert.ok(note, 'expected a fallback note when server is down')
|
|
// The fallback offers an Open-in-browser button.
|
|
const btns = findAllByClass(note, 'artifact-open')
|
|
assert.ok(btns.length >= 1)
|
|
})
|
|
|
|
test('inline preview: html url composed from base when event lacks url', async () => {
|
|
const { api, stream } = loadArtifacts({
|
|
getArtifactBase: async () => ({ url: 'http://127.0.0.1:7000' }),
|
|
})
|
|
api.onArtifactEvent({ artifactId: 'nested/page.html', kind: 'html', version: 1 })
|
|
const card = firstCard(stream)
|
|
findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
await tick()
|
|
const frame = findAllByClass(card, 'artifact-preview-frame')[0]
|
|
assert.ok(frame)
|
|
// encodeURIComponent keeps `/` as a path separator (server contract).
|
|
assert.equal(frame.src, 'http://127.0.0.1:7000/a/nested/page.html/')
|
|
})
|
|
|
|
// ---- behavior: per-card open-state memory -------------------------------
|
|
|
|
test('inline preview: open state remembered in the session bucket', () => {
|
|
const { api, stream } = loadArtifacts()
|
|
api.onArtifactEvent({ artifactId: 'mem.md', kind: 'md', version: 1, blob: '# x' })
|
|
const card = firstCard(stream)
|
|
const toggle = findAllByClass(card, 'artifact-preview-toggle')[0]
|
|
toggle.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
const bucket = api._bySession.get(api.getActiveSessionId())
|
|
assert.ok(bucket.previewOpen.has('mem.md'), 'expanding should record open state')
|
|
toggle.dispatch('click', { preventDefault() {}, stopPropagation() {} })
|
|
assert.ok(!bucket.previewOpen.has('mem.md'), 'collapsing should clear it')
|
|
})
|
|
|
|
// ---- source + css locks --------------------------------------------------
|
|
|
|
const artifactsSrc = fs.readFileSync(path.join(ROOT, 'src/renderer/artifacts.js'), 'utf8')
|
|
const styleCss = fs.readFileSync(path.join(ROOT, 'src/renderer/style.css'), 'utf8')
|
|
|
|
test('inline preview: iframe sandbox is allow-scripts only (no same-origin)', () => {
|
|
assert.match(artifactsSrc, /setAttribute\(['"]sandbox['"],\s*['"]allow-scripts['"]\)/)
|
|
// The sandbox VALUE must never grant same-origin. (The word may appear in
|
|
// an explanatory comment; guard the actual setAttribute argument instead.)
|
|
const sandboxCalls = artifactsSrc.match(/setAttribute\(['"]sandbox['"],\s*['"][^'"]*['"]\)/g) || []
|
|
for (const call of sandboxCalls) {
|
|
assert.ok(!/allow-same-origin/.test(call), 'sandbox must never grant allow-same-origin')
|
|
}
|
|
})
|
|
|
|
test('inline preview: md branch renders via __dshMdMini, never innerHTML', () => {
|
|
assert.match(artifactsSrc, /window\.__dshMdMini/)
|
|
// buildMdPreview / buildHtmlPreview must not assign innerHTML from content.
|
|
const previewRegion = artifactsSrc.slice(
|
|
artifactsSrc.indexOf('function buildMdPreview'),
|
|
artifactsSrc.indexOf('function invokeOpen'),
|
|
)
|
|
assert.ok(!/\.innerHTML\s*=/.test(previewRegion), 'no innerHTML in preview builders')
|
|
})
|
|
|
|
test('inline preview: CSS defines the preview + iframe blocks', () => {
|
|
assert.match(styleCss, /\.artifact-preview-toggle\s*\{/)
|
|
assert.match(styleCss, /\.artifact-preview-md\s*\{/)
|
|
assert.match(styleCss, /\.artifact-preview-frame\s*\{/)
|
|
assert.match(styleCss, /\.artifact-preview-frame[\s\S]{0,120}height:\s*360px/)
|
|
})
|