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.
481 lines
21 KiB
JavaScript
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) })
|