mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
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.
131 lines
7.1 KiB
JavaScript
131 lines
7.1 KiB
JavaScript
// Default profile: stdio-deepseek (2026-07-18 user directive)
|
|
//
|
|
// Team-lead single-line brief: 「默认档改为真模型档」. New downloaders should
|
|
// see a working DeepSeek reply on first send, not the echo bot. Persisted
|
|
// picks from prior sessions win over this default (readShellConfig().profile).
|
|
// Missing key → runtime dies with `llm-deepseek: an API key is required` in
|
|
// stderr; main.js accumulates stderr, matches the signature on crash, and
|
|
// forwards via runtime:error so classifyRuntimeError can render the guided
|
|
// switch card (locked separately in renderer-runtime-banner-classify.test.js).
|
|
//
|
|
// The tests below lock:
|
|
// (1) main.js has the new stdio-deepseek default at the module-scope var,
|
|
// (2) the boot-selection block reads shellConfig.profile before falling
|
|
// back to the default (so we don't stomp a persisted pick),
|
|
// (3) runtime:start persists the user's manual pick via writeShellConfig,
|
|
// (4) the crash handler in main.js forwards the api-key signature into
|
|
// runtime:error so the renderer's classifier can pick it up,
|
|
// (5) profiles.js/stdio-deepseek still carries the (needs DEEPSEEK_API_KEY)
|
|
// label — the settings/status-bar copy fans out from this one string.
|
|
//
|
|
// Static-audit pattern (regex-over-source), same shape as
|
|
// renderer-runtime-banner-classify.test.js. Real cold-start with/without a
|
|
// key is exercised in the interactive sweep v2 on real hardware.
|
|
|
|
'use strict'
|
|
|
|
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
|
|
const MAIN_PATH = path.join(__dirname, '..', 'src', 'main', 'main.js')
|
|
const PROFILES_PATH = path.join(__dirname, '..', 'src', 'main', 'profiles.js')
|
|
|
|
test('default profile is stdio-deepseek at module scope', () => {
|
|
const src = fs.readFileSync(MAIN_PATH, 'utf8')
|
|
// The `let currentProfileName = '<name>'` declaration must name
|
|
// stdio-deepseek. Regex is anchored to the exact assignment site so a
|
|
// rename or a stray shadowing declaration would fail loudly.
|
|
assert.match(
|
|
src,
|
|
/let\s+currentProfileName\s*=\s*'stdio-deepseek'/,
|
|
'currentProfileName must default to stdio-deepseek — the boss call is to aim first-run at the real model',
|
|
)
|
|
// Belt-and-suspenders: the OLD `daemon-echo` default must not survive as
|
|
// the module-scope initializer. Any reference inside comments/strings
|
|
// elsewhere is fine — only the `let currentProfileName = 'daemon-echo'`
|
|
// shape is banned.
|
|
assert.doesNotMatch(
|
|
src,
|
|
/let\s+currentProfileName\s*=\s*'daemon-echo'/,
|
|
'old daemon-echo default must not resurface as the module-scope initializer',
|
|
)
|
|
})
|
|
|
|
test('boot selects persisted profile first, stdio-deepseek fallback second', () => {
|
|
const src = fs.readFileSync(MAIN_PATH, 'utf8')
|
|
// The auto-start block must (a) read shellConfig, (b) prefer cfg.profile
|
|
// when the id is in listProfiles(), and (c) fall back to 'stdio-deepseek'
|
|
// as the boot target. Order matters — a persisted pick must win.
|
|
const idx = src.indexOf('bootProfile')
|
|
assert.notEqual(idx, -1, 'bootProfile selection missing from boot block')
|
|
const window = src.slice(Math.max(0, idx - 800), idx + 1200)
|
|
assert.match(window, /P\.readShellConfig\(\)/, 'must read shellConfig to honor a persisted pick')
|
|
assert.match(window, /cfg\.profile/, 'must consult cfg.profile as the persisted key')
|
|
assert.match(window, /listProfiles\(\)/, 'must validate persisted pick against listProfiles')
|
|
assert.match(window, /'stdio-deepseek'/, 'stdio-deepseek must be the fallback default in the boot block')
|
|
assert.match(window, /await startRuntime\(bootProfile\)/, 'startRuntime must be called with the selected bootProfile')
|
|
})
|
|
|
|
test('boot fallback on hard error goes to stdio-echo (keyless, no dev-clone required beyond jsonrpcBin)', () => {
|
|
const src = fs.readFileSync(MAIN_PATH, 'utf8')
|
|
// Kernel-level failures (spawn ENOENT, dev-clone missing) still land the
|
|
// shell on stdio-echo so the UI isn't dead. The api-key case does NOT
|
|
// hit this branch — the runtime spawns successfully, then dies during
|
|
// plugin init; the crash handler surfaces the classified error instead.
|
|
const idx = src.indexOf('boot failed, falling back to stdio-echo')
|
|
assert.notEqual(idx, -1, 'boot fallback message missing from main.js')
|
|
const window = src.slice(idx, idx + 400)
|
|
assert.match(window, /startRuntime\('stdio-echo'\)/, 'boot fallback must target stdio-echo')
|
|
})
|
|
|
|
test('runtime:start persists the user pick into shellConfig.profile', () => {
|
|
const src = fs.readFileSync(MAIN_PATH, 'utf8')
|
|
// The runtime:start handler must merge the picked name into shellConfig
|
|
// so next boot honors the pick. Best-effort — a fs failure must never
|
|
// block the runtime start itself.
|
|
const idx = src.indexOf(`ipcMain.handle('runtime:start'`)
|
|
assert.notEqual(idx, -1, 'runtime:start handler not found')
|
|
const body = src.slice(idx, idx + 800)
|
|
assert.match(body, /P\.writeShellConfig\(/, 'runtime:start must call writeShellConfig to persist')
|
|
assert.match(body, /profile:\s*name/, 'the persisted config must carry profile: name')
|
|
// Must guard the persistence so a fs error is non-fatal (try/catch or
|
|
// .catch on the fs promise — accept either shape).
|
|
assert.ok(
|
|
/try\s*{[\s\S]{0,400}writeShellConfig/.test(body),
|
|
'profile persistence must be inside try/catch to stay non-fatal',
|
|
)
|
|
})
|
|
|
|
test('crash handler forwards missing-api-key stderr signature to runtime:error', () => {
|
|
const src = fs.readFileSync(MAIN_PATH, 'utf8')
|
|
// The stderr accumulator + crash handler wiring — needed because
|
|
// llm-deepseek's key error surfaces on stderr, not via protocolError,
|
|
// and stderr is DSH_DEBUG-gated in the renderer. Regex scans for the
|
|
// named accumulator + the api-key signature + the runtime:error send.
|
|
const idx = src.indexOf('stderrAccum')
|
|
assert.notEqual(idx, -1, 'stderrAccum ledger missing — key error would not reach the banner')
|
|
// Widened window (2026-07-18, fix/harness-dev-guard): the crash handler
|
|
// gained a full-stderr log-file flush + a separate stderrFull
|
|
// accumulator between the declaration and the supervisor.on('stderr')
|
|
// wire. The original 2500-char window was tight; 4500 covers the
|
|
// expanded block while still failing if the wire actually moves out.
|
|
const window = src.slice(idx, idx + 4500)
|
|
assert.match(window, /supervisor\.on\('stderr'/, 'stderr accumulator must live inside a supervisor.on(stderr) handler wire')
|
|
assert.match(window, /API key is required/i, 'api-key signature must be matched in the crash handler')
|
|
assert.match(window, /send\('runtime:error'/, 'matched signature must be forwarded via runtime:error so classify can bucket it')
|
|
})
|
|
|
|
test('stdio-deepseek profile keeps the (needs DEEPSEEK_API_KEY) label', () => {
|
|
const src = fs.readFileSync(PROFILES_PATH, 'utf8')
|
|
// The dropdown, status bar chip, and settings pane all render this
|
|
// label. Locking here means a rename triggers a real trace of downstream
|
|
// UI copy (Settings copy, status-bar tooltip) rather than a silent drift.
|
|
assert.match(
|
|
src,
|
|
/stdio-deepseek[\s\S]{0,600}needs DEEPSEEK_API_KEY/,
|
|
'stdio-deepseek label must advertise the DEEPSEEK_API_KEY dependency',
|
|
)
|
|
})
|