// Artifact server unit tests. Runs under `node --test`, no Electron. // // Covers: // 1. isArtifactPath / pathToArtifactId / artifactIdToPath / parseArtifactUrl // — pure path matching, including traversal defence. // 2. preparePage — SSE snippet injection into a full doc, a fragment, and // a .md input. // 3. ArtifactServer end-to-end: // - starts on a random 127.0.0.1 port // - initial scan picks up pre-existing files // - fs.watch picks up a new file (event fires) // - GET /a// returns the page with the SSE snippet // - GET /events opens an SSE channel with the right headers, and a // subsequent artifact write broadcasts a reload event // - close() releases the port 'use strict' const test = require('node:test') const assert = require('node:assert/strict') const fs = require('node:fs') const path = require('node:path') const os = require('node:os') const http = require('node:http') const { ArtifactServer, isArtifactPath, pathToArtifactId, artifactIdToPath, parseArtifactUrl, preparePage, ARTIFACT_EXTS, } = require('../src/main/artifact-server.js') function tmpDir() { return fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-artifact-test-')) } function get(url, headers = {}) { return new Promise((resolve, reject) => { const req = http.get(url, { headers }, (res) => { const chunks = [] res.on('data', (c) => chunks.push(c)) res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body: Buffer.concat(chunks).toString('utf8') })) }) req.on('error', reject) req.setTimeout(2000, () => { req.destroy(new Error('http get timeout')); }) }) } // -- pure helpers ------------------------------------------------------------ test('isArtifactPath matches html/svg/md and rejects everything else', () => { assert.equal(isArtifactPath('foo.html'), true) assert.equal(isArtifactPath('foo.HTML'), true) assert.equal(isArtifactPath('/tmp/a/b/c.svg'), true) assert.equal(isArtifactPath('report.md'), true) assert.equal(isArtifactPath('foo.txt'), false) assert.equal(isArtifactPath('foo'), false) assert.equal(isArtifactPath(''), false) assert.equal(isArtifactPath(null), false) assert.equal(isArtifactPath(undefined), false) assert.equal(isArtifactPath(42), false) // Extensions we deliberately don't count: .htm, .xml, .txt. assert.equal(isArtifactPath('foo.htm'), false) }) test('pathToArtifactId returns null for paths outside the artifact dir', () => { const dir = '/workspace/.artifacts' assert.equal(pathToArtifactId('/workspace/.artifacts/report.html', dir), 'report.html') assert.equal(pathToArtifactId('/workspace/.artifacts/sub/dir/a.html', dir), 'sub/dir/a.html') assert.equal(pathToArtifactId('/workspace/other/report.html', dir), null) assert.equal(pathToArtifactId('/etc/passwd', dir), null) // The dir itself is not an artifact. assert.equal(pathToArtifactId('/workspace/.artifacts', dir), null) }) test('artifactIdToPath rejects traversal / absolute ids', () => { const dir = '/workspace/.artifacts' assert.equal(artifactIdToPath('report.html', dir), '/workspace/.artifacts/report.html') assert.equal(artifactIdToPath('sub/dir/a.html', dir), '/workspace/.artifacts/sub/dir/a.html') assert.equal(artifactIdToPath('../../etc/passwd', dir), null) assert.equal(artifactIdToPath('/etc/passwd', dir), null) assert.equal(artifactIdToPath('', dir), null) assert.equal(artifactIdToPath(null, dir), null) // URL-encoded traversal is also blocked. assert.equal(artifactIdToPath('..%2F..%2Fetc%2Fpasswd', dir), null) }) test('parseArtifactUrl extracts nested ids and rejects everything else', () => { assert.equal(parseArtifactUrl('/a/report.html/'), 'report.html') assert.equal(parseArtifactUrl('/a/report.html'), 'report.html') assert.equal(parseArtifactUrl('/a/sub/dir/a.html/'), 'sub/dir/a.html') assert.equal(parseArtifactUrl('/a/report.html?v=3'), 'report.html') assert.equal(parseArtifactUrl('/'), null) assert.equal(parseArtifactUrl('/events'), null) assert.equal(parseArtifactUrl('/a/'), null) assert.equal(parseArtifactUrl(''), null) }) test('preparePage injects the SSE snippet into a full document before ', () => { const src = '

hi

' const out = preparePage(src, '.html') assert.match(out, /new EventSource\('\/events'\)/) // Snippet must appear before , not after. const sseIdx = out.indexOf("new EventSource('/events')") const bodyIdx = out.indexOf('') assert.ok(sseIdx > 0 && sseIdx < bodyIdx, 'SSE snippet must appear before ') }) test('preparePage wraps a bare-fragment .html input in a skeleton', () => { const out = preparePage('

hi

', '.html') assert.match(out, /^hi<\/h1>/) assert.match(out, /new EventSource/) }) test('preparePage renders .md as an escaped
 and injects the snippet', () => {
  const out = preparePage('# hi\n', '.md')
  assert.match(out, /<script>/)   // escaped, not literal
  assert.doesNotMatch(out, /