Files
deepseek-harness/examples/desktop/test/layout-heuristics.test.js
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

227 lines
8.4 KiB
JavaScript

// Unit tests for the pure layout-hint engine. Runs under `node --test`.
// The module has no DOM/protocol/timer dependency, so we hand it plain
// SessionEvent fixtures and assert on the returned hint.
'use strict'
const test = require('node:test')
const assert = require('node:assert/strict')
const {
classifyTool,
signalsFromEvent,
aggregate,
computeHint,
LayoutHintTracker,
LAYOUTS,
} = require('../src/renderer/layout-heuristics.js')
// -- fixtures ---------------------------------------------------------------
let seq = 0
let now = 1_000_000
function toolCall(name) {
return { type: 'tool/call', seq: seq++, time: now++, data: { name, callId: `c${seq}` } }
}
function toolResult({ meta } = {}) {
return {
type: 'tool/result', seq: seq++, time: now++,
data: { callId: `c${seq}`, content: [{ type: 'text', text: 'ok' }], isError: false, meta },
}
}
function chunk(kind, text) {
return {
type: 'assistant/chunk', seq: seq++, time: now++,
data: { chunk: { type: kind + '-delta', text } },
}
}
function turnStart() { return { type: 'turn/start', seq: seq++, time: now++, data: {} } }
function turnEnd() { return { type: 'turn/end', seq: seq++, time: now++, data: {} } }
// -- classifyTool -----------------------------------------------------------
test('classifyTool buckets common tool names', () => {
assert.equal(classifyTool('edit_file'), 'diff')
assert.equal(classifyTool('str_replace_editor'), 'diff')
assert.equal(classifyTool('apply_patch'), 'diff')
assert.equal(classifyTool('MultiEdit'), 'diff')
assert.equal(classifyTool('write'), 'diff')
assert.equal(classifyTool('bash'), 'bash')
assert.equal(classifyTool('run_command'), 'bash')
assert.equal(classifyTool('shell'), 'bash')
assert.equal(classifyTool('artifact.create'), 'artifact')
assert.equal(classifyTool('show_artifact'), 'artifact')
assert.equal(classifyTool('read_file'), 'other')
assert.equal(classifyTool(''), null)
assert.equal(classifyTool(undefined), null)
})
// -- signalsFromEvent -------------------------------------------------------
test('signalsFromEvent maps only the events layouts care about', () => {
assert.equal(signalsFromEvent(null), null)
assert.equal(signalsFromEvent({ type: 'step/start' }), null)
assert.equal(signalsFromEvent({ type: 'request/header' }), null)
const s1 = signalsFromEvent(toolCall('edit_file'))
assert.equal(s1.kind, 'tool')
assert.equal(s1.tool, 'diff')
const s2 = signalsFromEvent(chunk('reasoning', 'thinking about it'))
assert.equal(s2.kind, 'reasoning')
assert.equal(s2.charCount, 'thinking about it'.length)
const artifactEv = toolResult({ meta: { card: 'artifact', artifact: { id: 'x' } } })
const s3 = signalsFromEvent(artifactEv)
assert.equal(s3.kind, 'tool')
assert.equal(s3.tool, 'artifact')
})
// -- computeHint (pure) -----------------------------------------------------
test('computeHint returns chat for empty windows', () => {
const empty = aggregate([])
assert.equal(computeHint(empty, empty, {}), 'chat')
})
test('computeHint returns code-review when diff tools dominate the window', () => {
const sigs = [
signalsFromEvent(toolCall('edit_file')),
signalsFromEvent(toolCall('edit_file')),
signalsFromEvent(toolCall('apply_patch')),
signalsFromEvent(toolCall('read_file')),
]
assert.equal(computeHint(aggregate(sigs), aggregate(sigs), {}), 'code-review')
})
test('computeHint does NOT flip to code-review on a single edit', () => {
const sigs = [
signalsFromEvent(toolCall('edit_file')),
signalsFromEvent(toolCall('read_file')),
signalsFromEvent(toolCall('read_file')),
]
assert.equal(computeHint(aggregate(sigs), aggregate(sigs), {}), 'chat')
})
test('computeHint returns monitor when bash is dense AND session is running', () => {
const sigs = [
signalsFromEvent(toolCall('bash')),
signalsFromEvent(toolCall('bash')),
signalsFromEvent(toolCall('shell')),
signalsFromEvent(toolCall('read_file')),
]
const full = aggregate(sigs)
assert.equal(computeHint(full, full, { running: true }), 'monitor')
// Same signal set without `running` and inside a short window stays chat.
assert.equal(computeHint(full, full, { running: false }), 'chat')
})
test('computeHint prefers artifact over everything when the recent slice has one', () => {
const sigs = [
signalsFromEvent(toolCall('edit_file')),
signalsFromEvent(toolCall('edit_file')),
signalsFromEvent(toolCall('apply_patch')),
signalsFromEvent(toolResult({ meta: { card: 'artifact' } })),
]
const full = aggregate(sigs)
const recent = aggregate(sigs.slice(-2))
assert.equal(computeHint(full, recent, {}), 'artifact')
})
test('computeHint drops artifact once it ages out of the recent slice', () => {
const sigs = [
signalsFromEvent(toolResult({ meta: { card: 'artifact' } })),
signalsFromEvent(toolCall('read_file')),
signalsFromEvent(toolCall('read_file')),
signalsFromEvent(toolCall('read_file')),
signalsFromEvent(toolCall('read_file')),
]
const full = aggregate(sigs)
const recent = aggregate(sigs.slice(-3)) // artifact aged out
assert.equal(computeHint(full, recent, {}), 'chat')
})
// -- LayoutHintTracker (debounce, lock, reset) ------------------------------
test('tracker sits on chat until N stable proposals promote a new hint', () => {
const t = new LayoutHintTracker({ stability: 3 })
// First diff tool: only 1 diff tool → doesn't meet diffToolsMin=2 yet, so
// computeHint proposes chat. Candidate stays null.
let r = t.push(toolCall('edit_file'))
assert.equal(r.hint, 'chat')
assert.equal(r.changed, false)
// Second edit: diffTools=2, ratio=1 → proposal=code-review (candidate=1).
r = t.push(toolCall('edit_file'))
assert.equal(r.hint, 'chat')
// Third: candidate=2.
r = t.push(toolCall('apply_patch'))
assert.equal(r.hint, 'chat')
// Fourth: candidate=3 → promote.
r = t.push(toolCall('edit_file'))
assert.equal(r.hint, 'code-review')
assert.equal(r.changed, true)
})
test('tracker debounce: a mixed jitter does NOT flip layouts', () => {
const t = new LayoutHintTracker({ stability: 3 })
// Alternating diff / non-diff — proposal never accumulates enough to fire.
const events = [
toolCall('edit_file'),
toolCall('read_file'),
toolCall('edit_file'),
toolCall('read_file'),
]
let lastHint = 'chat'
for (const ev of events) lastHint = t.push(ev).hint
assert.equal(lastHint, 'chat')
})
test('tracker manual lock wins over auto proposals', () => {
const t = new LayoutHintTracker({ stability: 2 })
t.lock('artifact')
// A cascade of diff events would normally promote code-review after 2 samples.
let r
for (let i = 0; i < 5; i++) r = t.push(toolCall('edit_file'))
assert.equal(r.hint, 'artifact')
assert.equal(r.changed, false)
// Unlock and let the queue resume; the accumulated diff signals still
// satisfy the ratio, so the next diff event promotes on the second sample.
t.unlock()
t.push(toolCall('edit_file'))
const after = t.push(toolCall('edit_file'))
assert.equal(after.hint, 'code-review')
})
test('tracker reset clears the window (used when switching sessions)', () => {
const t = new LayoutHintTracker({ stability: 2 })
t.push(toolCall('edit_file'))
t.push(toolCall('edit_file'))
t.push(toolCall('edit_file')) // promotes to code-review
assert.equal(t.currentHint(), 'code-review')
t.reset()
assert.equal(t.currentHint(), 'chat')
})
test('tracker exposes only the four documented layouts', () => {
assert.deepEqual([...LAYOUTS].sort(), ['artifact', 'chat', 'code-review', 'monitor'])
})
test('tracker.setMeta influences monitor gate (running=true relaxes windowSpan)', () => {
const t = new LayoutHintTracker({ stability: 2 })
t.setMeta({ running: true })
// bashToolsMin=3, so the third push is the first that proposes monitor;
// stability=2 needs one more of the same proposal to promote.
t.push(toolCall('bash'))
t.push(toolCall('bash'))
t.push(toolCall('shell'))
const r = t.push(toolCall('bash'))
assert.equal(r.hint, 'monitor')
})
test('artifact hint has priority over code-review even mid-debounce', () => {
const t = new LayoutHintTracker({ stability: 2 })
// build up diff signal
t.push(toolCall('edit_file'))
t.push(toolCall('edit_file'))
// then an artifact result appears — even without stability window growth
// (artifact only needs 2 stable samples in the recent slice).
t.push(toolResult({ meta: { card: 'artifact' } }))
const r = t.push(toolResult({ meta: { card: 'artifact' } }))
assert.equal(r.hint, 'artifact')
})