Files
deepseek-harness/examples/desktop/scripts/task-battery.mjs
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

481 lines
21 KiB
JavaScript

#!/usr/bin/env node
// Real-task completion battery — task #86 owner: passthrough-verify.
//
// Runs a ≥10-task suite against the running Electron on CDP :9299 with
// profile=stdio-deepseek (real DEEPSEEK_API_KEY). For each task:
// 1. New session, send prompt (real DeepSeek turn).
// 2. Poll turn/end; capture wire event count, tokens, chunks.
// 3. Objective completion judge (per task: file exists / content match /
// wire signal / etc).
// 4. Render assertions on the DOM: tool card presence, trace footer
// values, reasoning drawer availability, absence of "—" placeholders
// in the Tracing summary row for this session.
// 5. Collect captured errs from the pre-installed __battery hooks.
//
// LLM budget: ≤40 real calls. Sandbox workdir /tmp/dsh-task-battery/T??.
// Do not touch user's Electron (pid 91655) — this driver is pinned to
// CDP_PORT=9299.
import http from 'node:http'
import fs from 'node:fs'
import path from 'node:path'
import os from 'node:os'
const PORT = 9299
const SANDBOX_ROOT = '/tmp/dsh-task-battery'
function nowIso() { return new Date().toISOString() }
function log(...a) { console.log('[battery]', nowIso(), ...a) }
function listTargets() {
return new Promise((resolve, reject) => {
http.get(`http://localhost:${PORT}/json/list`, (res) => {
let b = ''
res.on('data', (c) => (b += c))
res.on('end', () => { try { resolve(JSON.parse(b)) } catch (e) { reject(e) } })
}).on('error', reject)
})
}
async function connect() {
const t = await listTargets()
const p = t.find((x) => x.title === 'DSH Desktop') || t.find((x) => x.type === 'page')
if (!p) throw new Error('no page')
const ws = new WebSocket(p.webSocketDebuggerUrl)
await new Promise((r, j) => { ws.onopen = () => r(); ws.onerror = (e) => j(e.message || 'ws err') })
let seq = 0
const pending = new Map()
ws.onmessage = (ev) => {
let msg; try { msg = JSON.parse(ev.data) } catch { return }
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(JSON.stringify(msg.error)))
else resolve(msg.result)
}
}
function send(method, params) {
const id = ++seq
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject })
ws.send(JSON.stringify({ id, method, params: params || {} }))
})
}
async function evalExpr(expr, awaitProm = true, timeoutMs = 60000) {
const r = await send('Runtime.evaluate', {
expression: expr, returnByValue: true, awaitPromise: awaitProm, timeout: timeoutMs,
})
if (r.exceptionDetails) {
const desc = r.exceptionDetails.exception?.description || r.exceptionDetails.text
throw new Error('eval-error: ' + desc)
}
return r.result && r.result.value
}
async function screenshot(pth) {
const r = await send('Page.captureScreenshot', { format: 'png' })
fs.writeFileSync(pth, Buffer.from(r.data, 'base64'))
return pth
}
return { ws, evalExpr, screenshot, close: () => ws.close() }
}
// Prompt DeepSeek to write files into an absolute workdir; the model can
// use its `bash` tool. We assert on real filesystem outcomes.
const TASKS = [
{
id: 'T01', title: 'single-file create (fizzbuzz)',
prompt: (wd) => `Use the bash tool to create a file at exactly ${wd}/fizzbuzz.py containing a Python fizzbuzz that prints 1..15. Then confirm.`,
judge: (wd) => {
const p = path.join(wd, 'fizzbuzz.py')
if (!fs.existsSync(p)) return { pass: false, why: 'file not created' }
const s = fs.readFileSync(p, 'utf8')
const looksLikeFizzbuzz = /fizz/i.test(s) && /buzz/i.test(s)
return { pass: looksLikeFizzbuzz, why: looksLikeFizzbuzz ? 'file has fizz/buzz' : 'file lacks fizz/buzz' }
},
},
{
id: 'T02', title: 'read file + answer',
setup: (wd) => fs.writeFileSync(path.join(wd, 'note.txt'), 'The secret color is turquoise. Do not forget.\n'),
prompt: (wd) => `Read the file at exactly ${wd}/note.txt using bash (cat), then tell me the secret color.`,
judge: (_wd, text) => {
const found = /turquoise/i.test(text || '')
return { pass: found, why: found ? 'answer contains turquoise' : 'answer lacks turquoise' }
},
},
{
id: 'T03', title: 'bash execute + summarize',
prompt: (wd) => `Run the bash command \`ls -la ${wd}\` and summarize what you see in one sentence.`,
judge: (_wd, text) => {
const mentions = /file|dir|empty|contents|entries|nothing/i.test(text || '')
return { pass: mentions, why: mentions ? 'summary reads like a listing summary' : 'summary does not describe a listing' }
},
},
{
id: 'T04', title: 'multi-step: three files + index',
prompt: (wd) => `Create three files with bash: ${wd}/a.txt (contents "alpha"), ${wd}/b.txt (contents "beta"), ${wd}/c.txt (contents "gamma"). Then create ${wd}/index.txt whose contents are the concatenation "alpha beta gamma".`,
judge: (wd) => {
const ok = ['a.txt', 'b.txt', 'c.txt', 'index.txt'].every((f) => fs.existsSync(path.join(wd, f)))
if (!ok) return { pass: false, why: 'missing file(s)' }
const idx = fs.readFileSync(path.join(wd, 'index.txt'), 'utf8')
const hasAll = /alpha/.test(idx) && /beta/.test(idx) && /gamma/.test(idx)
return { pass: hasAll, why: hasAll ? 'index has alpha/beta/gamma' : 'index missing tokens' }
},
},
{
id: 'T05', title: 'file edit (append line)',
setup: (wd) => fs.writeFileSync(path.join(wd, 'log.txt'), 'first line\n'),
prompt: (wd) => `Append the line "second line" to ${wd}/log.txt using bash. Keep the original content.`,
judge: (wd) => {
const s = fs.readFileSync(path.join(wd, 'log.txt'), 'utf8')
const both = /first line/.test(s) && /second line/.test(s)
return { pass: both, why: both ? 'both lines present' : `missing lines, got: ${JSON.stringify(s)}` }
},
},
{
id: 'T06', title: 'error recovery (nonexistent path)',
prompt: (wd) => `Try to \`cat /nonexistent/path-${Date.now()}\` using bash. When it fails, tell me in one sentence why it failed.`,
judge: (_wd, text) => {
const explains = /(no such|not found|does not exist|missing|nonexistent|doesn.t exist)/i.test(text || '')
return { pass: explains, why: explains ? 'model explained the failure' : 'model did not explain' }
},
},
{
id: 'T07', title: 'long output task',
prompt: (_wd) => `Write a Python one-liner in a code fence that prints 200 lines of increasing integers. Do not execute it; just show the code.`,
judge: (_wd, text) => {
const hasCode = /(```|for|range|print)/i.test(text || '')
return { pass: hasCode && (text || '').length > 60, why: hasCode ? 'response contains code' : 'no code visible' }
},
},
{
id: 'T08', title: 'cancel mid-turn',
// Long-running prompt we will cancel around 900ms in.
prompt: (_wd) => `Explain in extreme detail, at least 800 words, how the TCP three-way handshake works, including diagrams in ASCII.`,
cancelAfterMs: 900,
judge: (_wd, _text, meta) => {
const cancelled = meta.cancelResult && meta.cancelResult.cancelled === true
return { pass: cancelled, why: cancelled ? 'server acknowledged cancel' : 'cancel returned ' + JSON.stringify(meta.cancelResult) }
},
// No render assertions for the cancelled turn beyond "turn present in stream".
skipRenderChecks: false,
},
{
id: 'T09', title: 'fork replay from seq 1',
prompt: (wd) => `Say only the word "spark".`,
// After the turn, driver will fork from seq 1 and confirm shape.
fork: true,
judge: (_wd, text, meta) => {
const said = /spark/i.test(text || '')
const fk = meta.forkResult
const forked = fk && fk.childSessionId && (fk.mocked === false)
return { pass: said && forked, why: `saidSpark=${said} forked=${forked} mocked=${fk && fk.mocked}` }
},
},
{
id: 'T10', title: 'multi-turn follow-up',
prompt: (_wd) => `Remember the number 42 for later. Reply with just: OK.`,
followUp: {
prompt: 'What number did I ask you to remember?',
judge: (text) => ({ pass: /42/.test(text || ''), why: /42/.test(text || '') ? 'follow-up said 42' : 'follow-up missed 42' }),
},
judge: (_wd, text) => {
return { pass: /ok/i.test(text || ''), why: /ok/i.test(text || '') ? 'first turn said OK' : 'first turn did not say OK' }
},
},
]
async function ensureSandbox() {
fs.mkdirSync(SANDBOX_ROOT, { recursive: true })
}
async function newSessionWithCwd(c, _cwd) {
// newSession() ignores cwd (main.js:206 uses process.cwd); the sandbox is
// enforced by the prompt using absolute paths under SANDBOX_ROOT.
return c.evalExpr(`
(async () => {
const r = await window.dsh.newSession();
if (window.__dshRenderer && window.__dshRenderer.selectSession) {
window.__dshRenderer.selectSession(r.id);
}
return r;
})()
`)
}
async function sendPromptAndCollect(c, sid, prompt, { cancelAfterMs, budget = 60000 } = {}) {
// Fire send (do not await here — cancel test needs mid-flight control).
await c.evalExpr(`
(() => {
window.__task = { sendResult: null, sendErr: null };
window.__task.sendPromise = window.dsh.sendPrompt(${JSON.stringify(sid)}, ${JSON.stringify(prompt)})
.then(r => { window.__task.sendResult = r; return r; })
.catch(e => { window.__task.sendErr = String(e && e.message || e); });
return true;
})()
`, true, 10000)
let cancelResult = null
const start = Date.now()
if (cancelAfterMs) {
await new Promise((r) => setTimeout(r, cancelAfterMs))
cancelResult = await c.evalExpr(`
(async () => await window.dsh.cancelPrompt(${JSON.stringify(sid)}, 'battery cancel'))()
`, true, 15000)
}
// Poll for turn/end in the renderer's cachedEvents.
const eventsExpr = (sid) => `
(() => {
const st = window.__dshRenderer && window.__dshRenderer.snapshotState && window.__dshRenderer.snapshotState();
const meta = st && st.sessions && st.sessions.get ? st.sessions.get(${JSON.stringify(sid)}) : null;
const evs = (meta && meta.cachedEvents) || [];
const types = evs.map(e => e.type);
const done = types.includes('turn/end');
let text = '';
for (const e of evs) {
if (e.type === 'assistant/chunk' && e.content) {
const c = e.content;
if (typeof c === 'string') text += c;
else if (c.text) text += c.text;
else if (Array.isArray(c)) for (const b of c) if (b && b.type === 'text' && b.text) text += b.text;
}
if (e.type === 'assistant/message' && e.content) {
const c = e.content;
if (Array.isArray(c)) for (const b of c) if (b && b.type === 'text' && b.text) text += b.text;
else if (typeof c === 'string') text += c;
}
}
return { done, text, eventCount: evs.length, types };
})()
`
let lastSnap = { done: false, text: '', eventCount: 0, types: [] }
while (Date.now() - start < budget) {
lastSnap = await c.evalExpr(eventsExpr(sid))
if (lastSnap.done) break
if (cancelResult) {
const settled = await c.evalExpr('window.__task && (window.__task.sendErr || (window.__task.sendResult && window.__task.sendResult.cancelled))')
if (settled && lastSnap.eventCount > 0) {
// Give a beat for a final assistant/message before bailing.
await new Promise((r) => setTimeout(r, 800))
lastSnap = await c.evalExpr(eventsExpr(sid))
break
}
}
await new Promise((r) => setTimeout(r, 500))
}
const sendState = await c.evalExpr('({ sendResult: window.__task && window.__task.sendResult, sendErr: window.__task && window.__task.sendErr })')
const snapshot = {
text: lastSnap.text || '',
eventCount: lastSnap.eventCount || 0,
done: !!lastSnap.done,
types: lastSnap.types || [],
sendResult: sendState.sendResult || null,
sendErr: sendState.sendErr || null,
}
return { snapshot, cancelResult }
}
async function renderAssertions(c, sid, taskId) {
// Ask the renderer for tool-card / trace-footer / drawer state for this session.
const scoped = await c.evalExpr(`
(() => {
const s = document.getElementById('stream');
const html = s ? s.innerHTML : '';
// Placeholder scanner: 1969 timestamps, literal "—" chains, "[object Object]".
const has1969 = /1969-|Wed Dec 31 1969/.test(html);
const hasObjObj = /\\[object Object\\]/.test(html);
// Tool cards render as .tool-card (or generic .card with role=tool).
const toolCards = s ? s.querySelectorAll('.tool-card, [data-kind="tool"]').length : 0;
// Trace footer / drawer glyph presence.
const footerHasTokens = s ? !!s.querySelector('.trace-footer, .turn-footer, [data-role="trace-footer"]') : false;
// reasoning drawer / button presence.
const reasoningBtn = s ? !!s.querySelector('[data-tab="reasoning"], .reasoning-toggle') : false;
return { htmlLen: html.length, has1969, hasObjObj, toolCards, footerHasTokens, reasoningBtn };
})()
`)
// Tracing page row for this session (non-—).
const tracingRow = await c.evalExpr(`
(async () => {
// Switch to the tracing tab briefly to trigger render, then read.
if (window.__dshTabs && window.__dshTabs.switchTo) {
try { window.__dshTabs.switchTo('tracing'); } catch (_) {}
}
await new Promise(r => setTimeout(r, 800));
const table = document.querySelector('.tracing-table, [data-view="tracing"] table, table.tracing');
const rowCount = table ? table.querySelectorAll('tbody tr').length : 0;
// find row containing our session id
let ourRowText = null;
if (table) {
for (const tr of table.querySelectorAll('tbody tr')) {
if (tr.textContent && tr.textContent.indexOf(${JSON.stringify(sid.slice(0, 8))}) >= 0) {
ourRowText = tr.textContent.replace(/\\s+/g, ' ').trim();
break;
}
}
}
// switch back to chat
if (window.__dshTabs && window.__dshTabs.switchTo) {
try { window.__dshTabs.switchTo('chat'); } catch (_) {}
}
return { rowCount, ourRowText };
})()
`, true, 20000)
return { ...scoped, tracing: tracingRow, taskId }
}
async function collectErrors(c) {
return c.evalExpr('({ errs: (window.__battery && window.__battery.errs) || [], apiCalls: (window.__battery && window.__battery.apiCalls) || 0 })')
}
async function runTask(c, task, apiBudget) {
const wd = path.join(SANDBOX_ROOT, task.id)
fs.mkdirSync(wd, { recursive: true })
if (task.setup) task.setup(wd)
const sess = await newSessionWithCwd(c, wd)
const sid = sess.id
log(task.id, task.title, '→ sid', sid, 'wd', wd)
const prompt = task.prompt(wd)
const { snapshot, cancelResult } = await sendPromptAndCollect(c, sid, prompt, {
cancelAfterMs: task.cancelAfterMs,
budget: task.cancelAfterMs ? 20000 : 90000,
})
const meta = { cancelResult, forkResult: null }
if (task.fork) {
try {
meta.forkResult = await c.evalExpr(`
(async () => await window.dsh.forkSession({ sessionId: ${JSON.stringify(sid)}, boundary: { fromSeq: 1 } }))()
`, true, 15000)
} catch (e) { meta.forkResult = { error: String(e.message) } }
}
// Follow-up second turn (T10).
let followUpText = null
if (task.followUp) {
const fu = await sendPromptAndCollect(c, sid, task.followUp.prompt, { budget: 90000 })
followUpText = fu.snapshot.text
}
const verdict = task.judge(wd, snapshot.text, meta)
const followUpVerdict = task.followUp ? task.followUp.judge(followUpText) : null
const render = await renderAssertions(c, sid, task.id)
return {
id: task.id, title: task.title, sid, wd,
prompt, replyText: snapshot.text.slice(0, 400) + (snapshot.text.length > 400 ? '…' : ''),
replyLen: snapshot.text.length,
eventCount: snapshot.eventCount,
done: snapshot.done,
sendErr: snapshot.sendErr,
meta,
verdict,
followUpText: followUpText ? followUpText.slice(0, 300) : null,
followUpVerdict,
render,
}
}
async function buttonScan(c, someSid) {
// Select a real-data session and try clicking a set of common interactive elements.
await c.evalExpr(`window.__dshRenderer && window.__dshRenderer.selectSession && window.__dshRenderer.selectSession(${JSON.stringify(someSid)})`)
await new Promise((r) => setTimeout(r, 500))
const scan = await c.evalExpr(`
(() => {
const dead = [];
const clicked = [];
const s = document.getElementById('stream');
if (!s) return { dead: ['no-stream'], clicked: [] };
// 1. tool-card expand triggers
const expandables = Array.from(s.querySelectorAll('summary, [data-toggle], .expandable, .tool-card summary, details > summary'));
for (const el of expandables.slice(0, 10)) {
const before = el.closest('details') ? el.closest('details').open : null;
try {
el.click();
clicked.push({sel:'expandable', tag:el.tagName, before, after: el.closest('details') ? el.closest('details').open : null});
} catch (e) { dead.push({sel:'expandable-click', err: String(e.message)}); }
}
// 2. { } drawer buttons
const jsonBtns = Array.from(document.querySelectorAll('.copy-json, .open-json, [data-action="open-json"], [aria-label*="JSON" i]'));
for (const el of jsonBtns.slice(0, 5)) {
try { el.click(); clicked.push({sel:'json-btn', tag:el.tagName}); }
catch(e){ dead.push({sel:'json-btn', err: String(e.message)}); }
}
// 3. tab buttons in detail panes (Feedback/Input/Output/Attributes/Error)
const tabs = Array.from(document.querySelectorAll('.tab-button, [role="tab"]'));
for (const el of tabs.slice(0, 8)) {
try { el.click(); clicked.push({sel:'tab', tag:el.tagName, label:(el.textContent||'').trim().slice(0,20)}); }
catch(e){ dead.push({sel:'tab', err: String(e.message)}); }
}
// 4. copy buttons
const copyBtns = Array.from(document.querySelectorAll('.copy-btn, [data-action="copy"]'));
for (const el of copyBtns.slice(0, 5)) {
try { el.click(); clicked.push({sel:'copy', tag:el.tagName}); }
catch(e){ dead.push({sel:'copy', err: String(e.message)}); }
}
return { dead, clicked, counts: {
expandables: expandables.length, jsonBtns: jsonBtns.length, tabs: tabs.length, copyBtns: copyBtns.length,
} };
})()
`)
return scan
}
async function main() {
await ensureSandbox()
const c = await connect()
try {
log('runtimeStatus:')
const rs = await c.evalExpr('(async()=>await window.dsh.runtimeStatus())()')
console.log(JSON.stringify(rs, null, 2))
if (rs.profile !== 'stdio-deepseek') {
log('WARN: expected stdio-deepseek, got', rs.profile, '— switching')
await c.evalExpr('(async()=>await window.dsh.startRuntime("stdio-deepseek"))()')
// wait
for (let i = 0; i < 15; i++) {
await new Promise((r) => setTimeout(r, 3000))
const s = await c.evalExpr('(async()=>{const x=await window.dsh.runtimeStatus();return {status:x.status,profile:x.profile}})()')
log('poll', s)
if (s.status === 'running' && s.profile === 'stdio-deepseek') break
}
}
const results = []
for (const t of TASKS) {
try {
const r = await runTask(c, t, 40)
results.push(r)
log(t.id, 'verdict:', r.verdict.pass ? 'PASS' : 'FAIL', '—', r.verdict.why)
} catch (e) {
log(t.id, 'EXCEPTION', e.message)
results.push({ id: t.id, title: t.title, error: String(e.message), verdict: { pass: false, why: 'exception: ' + e.message } })
}
}
// Button scan on last session that had real data (prefer T01).
let scan = null
try {
const anySid = results.find((r) => r.sid && r.verdict.pass)?.sid || results[0]?.sid
if (anySid) scan = await buttonScan(c, anySid)
} catch (e) {
scan = { error: String(e.message) }
}
const errCollect = await collectErrors(c)
const report = {
timestamp: nowIso(),
profile: rs.profile,
model: rs.model,
results,
buttonScan: scan,
capturedErrors: errCollect.errs,
apiCallsCounted: errCollect.apiCalls,
passCount: results.filter((r) => r.verdict.pass).length,
totalTasks: results.length,
followUpPassCount: results.filter((r) => r.followUpVerdict && r.followUpVerdict.pass).length,
followUpTotal: results.filter((r) => r.followUpVerdict).length,
}
const outPath = path.join(SANDBOX_ROOT, 'battery-report.json')
fs.writeFileSync(outPath, JSON.stringify(report, null, 2))
log('wrote', outPath)
log('SUMMARY', report.passCount + '/' + report.totalTasks, 'PASS followUps', report.followUpPassCount + '/' + report.followUpTotal, ' apiCalls', report.apiCallsCounted, 'errs', report.capturedErrors.length)
} finally {
c.close()
}
}
main().catch((e) => { console.error(e.stack || e.message); process.exit(1) })