Files
deepseek-harness/examples/desktop/scripts/electron-e2e.driver.js
T

178 lines
6.9 KiB
JavaScript
Raw Normal View History

// Real-user E2E for the DSH desktop shell. Drives the running renderer via
// CDP and asserts both bug fixes.
//
// Usage: npx electron . --enable-logging --remote-debugging-port=9222
// node scripts/electron-e2e.driver.js
//
// Lives under scripts/ (not test/) so node --test's auto-discovery skips it
// — it needs a live Electron instance and would otherwise fail the suite.
'use strict'
async function main() {
const res = await fetch('http://localhost:9222/json/list')
const targets = await res.json()
const target = targets.find((t) => t.type === 'page' && (t.title || '').includes('DSH'))
if (!target) throw new Error('no DSH page target — is Electron running with --remote-debugging-port=9222?')
const ws = new WebSocket(target.webSocketDebuggerUrl)
await new Promise((r, x) => { ws.onopen = r; ws.onerror = x })
let nextId = 1
const pending = new Map()
ws.onmessage = (ev) => {
const msg = JSON.parse(ev.data)
if (msg.id != null && pending.has(msg.id)) {
const [resolve, reject] = pending.get(msg.id)
pending.delete(msg.id)
if (msg.error) reject(new Error(msg.error.message))
else resolve(msg.result)
}
}
const call = (method, params = {}) => new Promise((resolve, reject) => {
const id = nextId++
pending.set(id, [resolve, reject])
ws.send(JSON.stringify({ id, method, params }))
})
const evalJs = async (expr) => {
const r = await call('Runtime.evaluate', {
expression: expr, returnByValue: true, awaitPromise: true,
})
if (r.exceptionDetails) {
throw new Error(r.exceptionDetails.exception?.description ||
r.exceptionDetails.text || 'eval error')
}
return r.result && r.result.value
}
const fails = []
function assert(cond, msg) {
if (!cond) { fails.push(msg); console.error(' FAIL', msg) }
else console.log(' ok ', msg)
}
// Wait for the renderer to expose the debug seam
for (let i = 0; i < 20; i++) {
const ready = await evalJs(`!!(window.__dshRenderer && window.__dshRenderer.onSessionEvent)`)
if (ready) break
await new Promise((r) => setTimeout(r, 250))
}
// Switch to Chat tab
await evalJs(`window.__dshTabs && window.__dshTabs.switchTo && window.__dshTabs.switchTo('chat')`)
// ---- BUG 1 --------------------------------------------------------------
// Feed a synthetic user/message with the buggy shape (object source) and
// assert the rendered stream does NOT contain `[object Object]`. Also
// assert a request/header event doesn't leak into the chat.
const bug1 = await evalJs(`(() => {
const R = window.__dshRenderer
const sid = 'e2e-bug1-' + Date.now()
R.ensureSession(sid)
return R.selectSession(sid).then(() => {
// Case (a): user/message with object source — historical bug
R.onSessionEvent(sid, {
type: 'user/message',
seq: 1,
time: Date.now(),
data: {
source: { kind: 'plugin', plugin: 'compact' },
content: [{ type: 'text', text: 'context restored from compact summary' }],
},
})
// Case (b): a dev-only event that used to spam the chat
R.onSessionEvent(sid, {
type: 'request/header',
seq: 2,
time: Date.now(),
data: { model: 'echo-1', tokens: 42 },
})
// Case (c): a normal user/message from the user (control)
R.onSessionEvent(sid, {
type: 'user/message',
seq: 3,
time: Date.now(),
data: {
source: 'user',
content: [{ type: 'text', text: 'hello from real user test' }],
},
})
return {
text: R.getStreamText(),
html: R.getStreamHtml(),
}
})
})()`)
console.log('\n-- stream text after bug1 injection --')
console.log(bug1.text.slice(0, 500))
assert(!bug1.text.includes('[object Object]'), 'bug1: no [object Object] in chat stream')
assert(!bug1.text.includes('[[object Object]]'), 'bug1: no [[object Object]] wrapper')
assert(bug1.text.includes('[plugin:compact]'), 'bug1: object source rendered as [plugin:compact]')
assert(bug1.text.includes('context restored from compact summary'),
'bug1: compact source content shown')
assert(!bug1.text.includes('request/header'), 'bug1: request/header event NOT in chat')
assert(bug1.text.includes('hello from real user test'),
'bug1: normal user message still renders')
// ---- BUG 2 --------------------------------------------------------------
// Fill session A with a live conversation. Switch to a new session B.
// Switch back to A. Assert the conversation is still there — the daemon
// hasn't persisted anything (we didn't call it), so this exclusively
// exercises the in-memory cache path.
const bug2 = await evalJs(`(async () => {
const R = window.__dshRenderer
const sidA = 'e2e-bug2-A-' + Date.now()
const sidB = 'e2e-bug2-B-' + Date.now()
R.ensureSession(sidA)
R.ensureSession(sidB)
await R.selectSession(sidA)
// Simulate a full turn
R.onSessionEvent(sidA, {
type: 'user/message', seq: 1, time: Date.now(),
data: { source: 'user', content: [{ type: 'text', text: 'draw a heat map SVG' }] },
})
R.onSessionEvent(sidA, {
type: 'assistant/message', seq: 2, time: Date.now(),
data: { content: [{ type: 'text', text: 'here is the SVG source…' }] },
})
R.onSessionEvent(sidA, {
type: 'turn/end', seq: 3, time: Date.now(), data: { reason: { kind: 'end' } },
})
const beforeSwitch = R.getStreamText()
// Switch away, then back
await R.selectSession(sidB)
const afterSwitchAway = R.getStreamText()
await R.selectSession(sidA)
const afterSwitchBack = R.getStreamText()
return { beforeSwitch, afterSwitchAway, afterSwitchBack }
})()`)
console.log('\n-- bug2: before/after switch --')
console.log('before (in A):', bug2.beforeSwitch.slice(-200))
console.log('after switch away (B):', bug2.afterSwitchAway.slice(-200))
console.log('after switch back to A:', bug2.afterSwitchBack.slice(-300))
assert(bug2.beforeSwitch.includes('draw a heat map SVG'),
'bug2: user message present in session A before switch')
assert(bug2.beforeSwitch.includes('here is the SVG source'),
'bug2: assistant reply present before switch')
// After switch away, stream should be empty (B is fresh)
assert(!bug2.afterSwitchAway.includes('heat map SVG'),
'bug2: session B does not show A\'s history')
// After switching back, both messages must be there
assert(bug2.afterSwitchBack.includes('draw a heat map SVG'),
'bug2: user message restored after switch-back')
assert(bug2.afterSwitchBack.includes('here is the SVG source'),
'bug2: assistant reply restored after switch-back')
ws.close()
console.log(fails.length === 0 ? '\nall real-user E2E assertions passed.' : `\n${fails.length} failure(s).`)
process.exit(fails.length === 0 ? 0 : 1)
}
main().catch((err) => { console.error(err); process.exit(1) })