Files
deepseek-harness/examples/desktop/test/artifact-server.test.js
T
ZiyaZhang e8f5c0b51b feat(desktop): DSH Electron desktop shell — harness internals visualized
Minimal Electron shell over the DSH JSON-RPC runtime — a first-look at
what a ChatGPT.app-style host on top of the DeepSeek Harness looks
like, with the harness's normally-invisible internals (trace timeline,
context surface, subagent tree, compaction, plugin registry, rubrics)
brought forward as first-class UI surfaces so plugin authors and
researchers can see what the agent is actually doing.

Runs against three keyless-to-live profiles (stdio-echo works on
master out of the box; daemon-echo / daemon-vibe-echo activate once
the daemon-demo lands; stdio-deepseek and daemon-vibe hit the real
DeepSeek API when you supply a key). HARNESS_DEV auto-resolves to the
in-repo runtime when this shell ships under examples/desktop/, so a
fresh clone launches without config; env DSH_DEV_ROOT overrides for
custom layouts, and a sibling deepseek-harness-dev/ checkout is the
original dev workflow.

Cold-clone gate (P0 fixes for first-time-clone usability):
- HARNESS_DEV: 3-candidate resolver (env → walk-up in-repo marker →
  sibling), unit-tested via mock fs so ordering is locked without
  needing either real layout on disk.
- config yml leaves rewritten at assemble time so the sibling-clone
  paths (../../deepseek-harness-dev/examples/echo-agent/…) become
  the in-repo paths (../../echo-agent/…) in the released tree —
  source yml stays usable for local dev, released tree ships a
  working shape.
- pnpm-workspace.yaml allowBuilds.electron = true (was placeholder).
- missing-key card in stdio-deepseek offers a one-click switch to
  stdio-echo (the keyless profile that works on master) rather than
  daemon-echo (blocked on the not-yet-shipped daemon-demo).
- assemble-oss-release.sh rewrites the source-side breadcrumb name
  'dsh-desktop-demo' → 'dsh-desktop' for the released package.json.

FOUC guard on the onboarding gate (41fc5df carried) keeps the
first-launch splash from flashing before the runtime probe finishes.

Test suite (1634 tests in source, 3990 in the runtime repo) covers
resolver ordering, renderer classifiers, trace timeline shape,
compaction diff rendering, rubric parity, and the missing-key
onboarding paths.
2026-07-18 12:59:34 -07:00

294 lines
11 KiB
JavaScript

// 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/<id>/ 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 </body>', () => {
const src = '<!doctype html><html><head></head><body><h1>hi</h1></body></html>'
const out = preparePage(src, '.html')
assert.match(out, /new EventSource\('\/events'\)/)
// Snippet must appear before </body>, not after.
const sseIdx = out.indexOf("new EventSource('/events')")
const bodyIdx = out.indexOf('</body>')
assert.ok(sseIdx > 0 && sseIdx < bodyIdx, 'SSE snippet must appear before </body>')
})
test('preparePage wraps a bare-fragment .html input in a skeleton', () => {
const out = preparePage('<h1>hi</h1>', '.html')
assert.match(out, /^<!doctype html/i)
assert.match(out, /<h1>hi<\/h1>/)
assert.match(out, /new EventSource/)
})
test('preparePage renders .md as an escaped <pre> and injects the snippet', () => {
const out = preparePage('# hi\n<script>alert(1)</script>', '.md')
assert.match(out, /&lt;script&gt;/) // escaped, not literal
assert.doesNotMatch(out, /<script>alert\(1\)<\/script>/)
assert.match(out, /new EventSource/)
})
test('preparePage passes .svg through untouched (served with the svg MIME)', () => {
const svg = '<svg xmlns="http://www.w3.org/2000/svg"><circle r="5"/></svg>'
assert.equal(preparePage(svg, '.svg'), svg)
})
test('ARTIFACT_EXTS is the source of truth', () => {
assert.deepEqual([...ARTIFACT_EXTS].sort(), ['.html', '.md', '.svg'])
})
// -- server integration ------------------------------------------------------
test('ArtifactServer starts on 127.0.0.1 with an ephemeral port', async () => {
const dir = tmpDir()
const s = new ArtifactServer({ artifactDir: dir })
try {
await s.ensureStarted()
assert.ok(s.port > 0)
assert.equal(s.host, '127.0.0.1')
assert.match(s.baseUrl(), /^http:\/\/127\.0\.0\.1:\d+$/)
// /health responds with a JSON payload.
const r = await get(s.baseUrl() + '/health')
assert.equal(r.status, 200)
assert.match(r.headers['content-type'], /application\/json/)
assert.match(r.body, /"ok":true/)
} finally {
await s.close()
}
})
test('ArtifactServer serves an existing HTML file with the SSE snippet injected', async () => {
const dir = tmpDir()
fs.writeFileSync(path.join(dir, 'report.html'), '<!doctype html><html><body><h1>hello</h1></body></html>')
const s = new ArtifactServer({ artifactDir: dir })
try {
await s.ensureStarted()
// The initial scan populates knownArtifacts.
assert.ok(s.knownArtifacts.has('report.html'), 'initial scan should register the file')
const r = await get(s.urlFor('report.html'))
assert.equal(r.status, 200)
assert.match(r.headers['content-type'], /text\/html/)
assert.match(r.body, /<h1>hello<\/h1>/)
assert.match(r.body, /new EventSource\('\/events'\)/)
// No-store cache header so live reload always fetches fresh.
assert.match(r.headers['cache-control'] || '', /no-store/)
} finally {
await s.close()
}
})
test('ArtifactServer rejects traversal / non-existent artifact ids', async () => {
const dir = tmpDir()
const s = new ArtifactServer({ artifactDir: dir })
try {
await s.ensureStarted()
const bad = await get(s.baseUrl() + '/a/..%2F..%2Fetc%2Fpasswd/')
assert.equal(bad.status, 400)
const missing = await get(s.baseUrl() + '/a/nope.html/')
assert.equal(missing.status, 404)
const junk = await get(s.baseUrl() + '/nope')
assert.equal(junk.status, 404)
} finally {
await s.close()
}
})
test('SSE /events opens the right headers and broadcasts reload on artifact update', async () => {
const dir = tmpDir()
const s = new ArtifactServer({ artifactDir: dir })
try {
await s.ensureStarted()
// Open an SSE connection manually so we can read the raw stream.
const url = new URL(s.baseUrl() + '/events')
const chunks = []
const req = http.get({ hostname: url.hostname, port: url.port, path: url.pathname })
const resPromise = new Promise((resolve) => req.on('response', resolve))
const res = await resPromise
assert.equal(res.statusCode, 200)
assert.match(res.headers['content-type'], /text\/event-stream/)
assert.match(res.headers['cache-control'] || '', /no-store/)
res.on('data', (c) => chunks.push(c.toString('utf8')))
// Wait a beat for the SSE handshake payload (`retry: 300`) and for the
// server to register our connection.
await new Promise((r) => setTimeout(r, 30))
assert.ok(s.clients.size >= 1, 'server should have registered the SSE client')
// Poking the server directly to sidestep fs.watch flakiness under CI.
const abs = path.join(dir, 'live.html')
fs.writeFileSync(abs, '<!doctype html><html><body>v1</body></html>')
s._noteArtifact(abs, 'test')
// Give the broadcast a moment.
await new Promise((r) => setTimeout(r, 30))
const raw = chunks.join('')
assert.match(raw, /event: reload/)
assert.match(raw, /"artifactId":"live\.html"/)
assert.match(raw, /"version":1/)
// Second update bumps the version.
fs.writeFileSync(abs, '<!doctype html><html><body>v2</body></html>')
s._noteArtifact(abs, 'test')
await new Promise((r) => setTimeout(r, 30))
const raw2 = chunks.join('')
assert.match(raw2, /"version":2/)
req.destroy()
} finally {
await s.close()
}
})
test('artifact event is emitted with the resolved URL and version', async () => {
const dir = tmpDir()
const s = new ArtifactServer({ artifactDir: dir })
const events = []
s.on('artifact', (e) => events.push(e))
try {
await s.ensureStarted()
const abs = path.join(dir, 'x.svg')
fs.writeFileSync(abs, '<svg xmlns="http://www.w3.org/2000/svg"></svg>')
s._noteArtifact(abs, 'test')
assert.equal(events.length, 1)
assert.equal(events[0].artifactId, 'x.svg')
assert.equal(events[0].kind, 'svg')
assert.equal(events[0].version, 1)
assert.match(events[0].url, /\/a\/x\.svg\//)
s._noteArtifact(abs, 'test')
assert.equal(events[1].version, 2)
} finally {
await s.close()
}
})
test('close() releases the port', async () => {
const dir = tmpDir()
const s = new ArtifactServer({ artifactDir: dir })
await s.ensureStarted()
const port = s.port
await s.close()
// Trying to GET now should fail (connection refused). We give the OS a
// moment to release the port.
await new Promise((r) => setTimeout(r, 50))
await assert.rejects(get(`http://127.0.0.1:${port}/health`))
})
test('extractPathCandidates pulls file paths from tool/result shapes', () => {
const { _internal } = require('../src/main/artifact-ipc.js')
const { extractPathCandidates } = _internal
assert.deepEqual(extractPathCandidates({ filePath: '/w/.artifacts/a.html' }), ['/w/.artifacts/a.html'])
assert.deepEqual(extractPathCandidates({ meta: { path: '/w/.artifacts/b.svg' } }), ['/w/.artifacts/b.svg'])
assert.deepEqual(
extractPathCandidates({ content: [{ type: 'text', text: 'wrote to ./.artifacts/c.md today' }] }),
['./.artifacts/c.md'],
)
// No text-block false positives on non-artifact extensions.
assert.deepEqual(
extractPathCandidates({ content: [{ type: 'text', text: 'saw file.txt earlier' }] }),
[],
)
// Both structured and text extraction can coexist.
const both = extractPathCandidates({
filePath: '/w/.artifacts/a.html',
content: [{ type: 'text', text: 'also wrote /w/.artifacts/b.md' }],
})
assert.ok(both.includes('/w/.artifacts/a.html'))
assert.ok(both.includes('/w/.artifacts/b.md'))
})