Merge origin/master into worktree/install-interface-choice

This commit is contained in:
Tianyi Cui
2026-08-01 18:06:49 +08:00
168 changed files with 3390 additions and 644 deletions

View File

@@ -1,18 +1,18 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { parseDshArgs } from '../src/args.ts'
const parse = (argv: string[]) => parseDshArgs(argv, '1.2.3')
const parse = (argv: string[], experimentalEnv = false) => parseDshArgs(argv, '1.2.3', experimentalEnv)
/**
* `parseDshArgs` calls `process.exit` for `--help`/`--version`/errors and lets
* Commander print to the real streams; capture the exit code and mute output.
*/
function exitCode(argv: string[]): number {
function exitCode(argv: string[], experimentalEnv = false): number {
const exit = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('exit') })
vi.spyOn(process.stdout, 'write').mockReturnValue(true)
vi.spyOn(process.stderr, 'write').mockReturnValue(true)
try {
parse(argv)
parse(argv, experimentalEnv)
throw new Error(`expected ${JSON.stringify(argv)} to exit`)
} catch {
return exit.mock.calls.at(-1)?.[0] as number
@@ -24,13 +24,15 @@ function exitCode(argv: string[]): number {
afterEach(() => { vi.restoreAllMocks() })
describe('parseDshArgs', () => {
it('routes each mode by its shape: default TUI, -p headless, meta and web subcommands', () => {
it('routes each mode by its shape: default TUI, -p headless, experimental and web subcommands', () => {
expect(parse([])).toEqual({ mode: 'tui' })
expect(parse(['--config', 'custom.yml'])).toEqual({ mode: 'tui', config: 'custom.yml' })
expect(parse(['--config-replace', 'tree.yml'])).toEqual({ mode: 'tui', configReplace: 'tree.yml' })
expect(parse(['--resume', 'sess', '--config', 'app.yml'])).toEqual({ mode: 'tui', config: 'app.yml', resume: 'sess' })
expect(parse(['-p', 'do the thing'])).toEqual({ mode: 'headless', prompt: 'do the thing' })
expect(parse(['meta'])).toEqual({ mode: 'meta' })
// Experimental subcommands run under the per-invocation flag or the env opt-in.
expect(parse(['meta', '--experimental'])).toEqual({ mode: 'meta' })
expect(parse(['meta'], true)).toEqual({ mode: 'meta' })
// Bare `web` carries no host/port: the shipped Web overlay owns the default.
expect(parse(['web'])).toEqual({ mode: 'web', dev: false })
expect(parse(['web', '--config', 'web.yml'])).toEqual({ mode: 'web', dev: false, config: 'web.yml' })
@@ -39,7 +41,8 @@ describe('parseDshArgs', () => {
expect(parse(['web', '--host', '0.0.0.0', '--port', '8080', '--dev', '--workspace-root', '/w']))
.toEqual({ mode: 'web', host: '0.0.0.0', port: 8080, dev: true, workspaceRoot: '/w' })
// Guided fresh-session entries carry nothing: bare mode discriminant only.
expect(parse(['upgrade'])).toEqual({ mode: 'upgrade' })
expect(parse(['upgrade', '--experimental'])).toEqual({ mode: 'upgrade' })
expect(parse(['upgrade'], true)).toEqual({ mode: 'upgrade' })
// --trusted-host is variadic and repeatable; authorities pass through unvalidated.
expect(parse(['web', '--trusted-host', 'harness.internal:3080', 'lab.internal', '--trusted-host', '10.0.0.9']))
.toEqual({ mode: 'web', dev: false, trustedHosts: ['harness.internal:3080', 'lab.internal', '10.0.0.9'] })
@@ -64,8 +67,8 @@ describe('parseDshArgs', () => {
expect(exitCode(['web', '--dump-config', '--dump-default-config'])).toBe(1)
expect(exitCode(['web', '--dump-default-config', '--config', 'w.yml'])).toBe(1)
// A leaked dump flag on a subcommand that has none is a mistyped invocation.
expect(exitCode(['meta', '--dump-config'])).toBe(1)
expect(exitCode(['upgrade', '--dump-config'])).toBe(1)
expect(exitCode(['meta', '--experimental', '--dump-config'])).toBe(1)
expect(exitCode(['upgrade', '--experimental', '--dump-config'])).toBe(1)
})
it('exits nonzero instead of silently starting fresh or dropping inputs', () => {
@@ -88,17 +91,32 @@ describe('parseDshArgs', () => {
expect(exitCode(['--config-replace', 'tree.yml', 'web'])).toBe(1)
// Same rule for each subcommand that shares no option with the default
// surface, so a leaked flag is a typo, not something to ignore.
// `meta` fixes its own config tree and always starts fresh, so every
// default-surface option is rejected.
expect(exitCode(['meta', '--resume', 's'])).toBe(1)
expect(exitCode(['meta', '--config', 'c.yml'])).toBe(1)
expect(exitCode(['meta', '--config-replace', 'tree.yml'])).toBe(1)
expect(exitCode(['meta', '-p', 'task'])).toBe(1)
// `upgrade` takes no options: any leaked default-surface flag is a
// mistyped invocation, not a silently-dropped input.
expect(exitCode(['upgrade', '--resume', 's'])).toBe(1)
expect(exitCode(['upgrade', '--config', 'c.yml'])).toBe(1)
expect(exitCode(['-p', 'task', 'upgrade'])).toBe(1)
// `meta` fixes its own config tree and always starts fresh,
// so every default-surface option is rejected.
expect(exitCode(['meta', '--experimental', '--resume', 's'])).toBe(1)
expect(exitCode(['meta', '--experimental', '--config', 'c.yml'])).toBe(1)
expect(exitCode(['meta', '--experimental', '--config-replace', 'tree.yml'])).toBe(1)
expect(exitCode(['meta', '--experimental', '-p', 'task'])).toBe(1)
// `upgrade` takes no options beyond the gate: any leaked default-surface
// flag is a mistyped invocation, not a silently-dropped input.
expect(exitCode(['upgrade', '--experimental', '--resume', 's'])).toBe(1)
expect(exitCode(['upgrade', '--experimental', '--config', 'c.yml'])).toBe(1)
expect(exitCode(['-p', 'task', 'upgrade', '--experimental'])).toBe(1)
// The pre-release command names have no compatibility aliases.
expect(exitCode(['experimental-meta'])).toBe(1)
expect(exitCode(['experimental-upgrade'])).toBe(1)
})
it('gates experimental subcommands behind --experimental or the env opt-in', () => {
// Bare `meta`/`upgrade` without either opt-in must fail loud, not run.
expect(exitCode(['meta'])).toBe(1)
expect(exitCode(['upgrade'])).toBe(1)
// A leaked default-surface flag stays a typo even when the gate is passed
// by the environment alone.
expect(exitCode(['meta', '--resume', 's'], true)).toBe(1)
// The flag and the env opt-in may coexist.
expect(parse(['meta', '--experimental'], true)).toEqual({ mode: 'meta' })
expect(parse(['upgrade', '--experimental'], true)).toEqual({ mode: 'upgrade' })
})
it('exits 0 for --help (disclosing web) and --version', () => {

View File

@@ -54,6 +54,31 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)',
expect(stdout).toBe('')
}, 30_000)
describe('experimental subcommand gate', () => {
// The gate has two halves: a per-invocation --experimental flag parsed by
// Commander and an env opt-in read by bin.ts as exactly '1'. Passing the
// gate is proven by reaching the NEXT failure — the TUI's piped-stdio
// refusal — instead of the gate diagnostic.
it('rejects bare `meta`/`upgrade` LOUD, naming both opt-ins', async () => {
for (const command of ['meta', 'upgrade']) {
const { code, stderr } = await runBuiltBin([command], { DSH_EXPERIMENTAL: '' })
expect(code).toBe(1)
expect(stderr).toContain(`${command} is experimental; pass --experimental or set DSH_EXPERIMENTAL=1`)
}
}, 30_000)
it('admits --experimental and DSH_EXPERIMENTAL=1, but not other env values', async () => {
const flagged = await runBuiltBin(['meta', '--experimental'], { DSH_EXPERIMENTAL: '' })
expect(flagged.stderr).toContain('requires stdin and stdout to be interactive TTYs')
const env = await runBuiltBin(['meta'], { DSH_EXPERIMENTAL: '1' })
expect(env.stderr).toContain('requires stdin and stdout to be interactive TTYs')
// The env opt-in is exact: '0' (or any other value) does not enable.
const zero = await runBuiltBin(['meta'], { DSH_EXPERIMENTAL: '0' })
expect(zero.code).toBe(1)
expect(zero.stderr).toContain('meta is experimental')
}, 30_000)
})
describe('dsh --dump-config', () => {
let home: string
beforeEach(() => { home = mkdtempSync(join(tmpdir(), 'dsh-dump-bin-')) })

View File

@@ -6,11 +6,11 @@ import { execa } from 'execa'
import { resolveExampleLaunch, type ExampleLaunch } from '@deepseek-ai/dsh-loader-smoke'
const POSIX_PTY_DRIVER = String.raw`
import errno, json, os, pty, select, signal, sys, time
node, launch_args_json, launch_env_json, cwd, actions_json, expected_exit, timeout_seconds = sys.argv[1:]
import errno, fcntl, json, os, pty, select, signal, struct, sys, termios, time
node, launch_args_json, launch_env_json, cwd, actions_json, expected_exit, timeout_seconds, columns, rows = sys.argv[1:]
env = os.environ.copy()
env.update(json.loads(launch_env_json))
env.update({"COLUMNS": "100", "LINES": "30"})
env.update({"COLUMNS": columns, "LINES": rows})
# Deterministic banner: a developer shell's COLORTERM=truecolor would switch the
# banner to the per-letter gradient (one SGR per letter), breaking literal
# DEEPSEEK assertions. The gradient path has its own unit and snapshot coverage.
@@ -20,6 +20,7 @@ pid, fd = pty.fork()
if pid == 0:
os.chdir(cwd)
os.execvpe(node, [node, *json.loads(launch_args_json)], env)
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", int(rows), int(columns), 0, 0))
output = bytearray()
action_index = 0
@@ -36,9 +37,14 @@ while time.monotonic() < deadline:
chunk = b""
if chunk:
output.extend(chunk)
while action_index < len(actions) and actions[action_index]["waitFor"].encode() in output:
while action_index < len(actions):
marker = actions[action_index]["waitFor"].encode()
if output.count(marker) < actions[action_index].get("occurrence", 1):
break
action = actions[action_index]
if "writeFile" in action:
if "signal" in action:
os.kill(pid, getattr(signal, action["signal"]))
elif "writeFile" in action:
target = os.path.join(cwd, action["writeFile"]["path"])
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, "w", encoding="utf-8") as handle:
@@ -68,9 +74,15 @@ if actual_exit != int(expected_exit):
/** One terminal input or workspace mutation performed after its marker renders. */
type TuiPtyAction =
| { readonly waitFor: string; readonly send: string }
| {
readonly waitFor: string
readonly occurrence?: number
readonly send: string
}
| { readonly waitFor: string; readonly occurrence?: number; readonly signal: 'SIGTERM' }
| {
readonly waitFor: string
readonly occurrence?: number
readonly writeFile: { readonly path: string; readonly content: string }
readonly send?: string
}
@@ -89,6 +101,12 @@ export interface TuiPtySmokeOptions {
readonly env?: Readonly<NodeJS.ProcessEnv>
readonly expectedExitCode?: number
readonly timeoutMs?: number
/** Existing isolated workspace to reuse; when omitted the harness creates and removes one. */
readonly cwd?: string
/** Pseudo-terminal columns; defaults to 100. */
readonly columns?: number
/** Pseudo-terminal rows; defaults to 30. */
readonly rows?: number
/** Seed the isolated workspace (`cwd`, with `$DSH_HOME` at `.dsh` and the agents home at `.agents`) before launch. */
readonly prepare?: (cwd: string) => Promise<void>
/** Inspect the workspace after a passing run, before the temp dir is removed. */
@@ -119,6 +137,8 @@ async function runPosixPtySmoke(
JSON.stringify(options.actions ?? []),
String(options.expectedExitCode ?? 0),
String(timeoutMs / 1_000),
String(options.columns ?? 100),
String(options.rows ?? 30),
], {
stdin: 'ignore',
timeout: timeoutMs + 5_000,
@@ -150,8 +170,8 @@ async function runWindowsPtySmoke(
let timedOut = false
const terminal = pty.spawn(launch.command, launch.args, {
name: 'xterm-256color',
cols: 100,
rows: 30,
cols: options.columns ?? 100,
rows: options.rows ?? 30,
cwd,
env: definedEnv({
...process.env,
@@ -159,8 +179,8 @@ async function runWindowsPtySmoke(
// Match the POSIX driver: no COLORTERM, so the banner never takes the
// truecolor gradient path under a developer's shell.
COLORTERM: undefined,
COLUMNS: '100',
LINES: '30',
COLUMNS: String(options.columns ?? 100),
LINES: String(options.rows ?? 30),
}),
})
const timer = setTimeout(() => {
@@ -169,9 +189,14 @@ async function runWindowsPtySmoke(
}, timeoutMs)
terminal.onData((chunk) => {
output += chunk
while (actionIndex < actions.length && output.includes(actions[actionIndex]!.waitFor)) {
while (
actionIndex < actions.length
&& output.split(actions[actionIndex]!.waitFor).length - 1 >= (actions[actionIndex]!.occurrence ?? 1)
) {
const action = actions[actionIndex]!
if ('writeFile' in action) {
if ('signal' in action) {
terminal.kill(action.signal)
} else if ('writeFile' in action) {
const target = join(cwd, action.writeFile.path)
mkdirSync(dirname(target), { recursive: true })
writeFileSync(target, action.writeFile.content)
@@ -205,7 +230,8 @@ async function runWindowsPtySmoke(
* @returns complete pseudo-terminal output.
*/
export async function runTuiPtySmoke(options: TuiPtySmokeOptions): Promise<string> {
const cwd = await mkdtemp(join(tmpdir(), options.tempDirPrefix))
const ownedCwd = options.cwd === undefined
const cwd = options.cwd ?? await mkdtemp(join(tmpdir(), options.tempDirPrefix))
const timeoutMs = options.timeoutMs ?? 25_000
try {
await options.prepare?.(cwd)
@@ -231,6 +257,6 @@ export async function runTuiPtySmoke(options: TuiPtySmokeOptions): Promise<strin
await options.inspect?.(cwd)
return output
} finally {
await rm(cwd, { recursive: true, force: true })
if (ownedCwd) await rm(cwd, { recursive: true, force: true })
}
}

View File

@@ -7,6 +7,7 @@ import type { SessionEvent } from '@deepseek-ai/dsh-session'
import { COMPOSITION_REPLY_TEXT } from './fixtures/composition-echo-llm.ts'
import { COMPOSITION_SETTLED_MARKER } from './fixtures/composition-settled.ts'
import { runTuiPtySmoke } from './pty-harness.ts'
import { acknowledgeTuiFirstRunWelcome } from '../src/tui-onboarding/tui-first-run-welcome.ts'
const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
@@ -105,6 +106,7 @@ describe('shipped dsh composition (real Loader tree in a PTY)', () => {
tsconfigPath,
configPath: keylessTail,
env: { DEEPSEEK_API_KEY: 'keyless-composition-no-call', DSH_TELEMETRY_DISABLED: '1' },
prepare: cwd => acknowledgeTuiFirstRunWelcome(join(cwd, '.dsh')),
// Artifact CI builds and smokes concurrently on a contended runner.
...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
actions: [

View File

@@ -0,0 +1,30 @@
{"type":"session","version":0,"id":"main-session","createdAt":1784606400000,"cwd":"{{cwd}}"}
{"type":"turn/start","seq":0,"time":1784606400000,"data":{"turn":1,"trigger":{"kind":"message","source":{"kind":"user"}}}}
{"type":"user/message","seq":1,"time":1784606400000,"data":{"content":[{"type":"text","text":"what's the workdir?"}],"source":{"kind":"user"},"role":"user","id":"3fdc2885-1bea-4c6c-b4af-dbd5af7594f8"},"surfaceOp":"append"}
{"type":"session/title","seq":2,"time":1784606400000,"data":{"title":"what's the workdir?","messageSeqs":[1],"source":{"kind":"fallback"}}}
{"type":"step/start","seq":3,"time":1784606400000,"data":{"turn":1,"step":1}}
{"type":"request/header","seq":4,"time":1784606400000,"data":{"header":{"config":{"provider":"deepseek","model":"deepseek-v4-flash","reasoningEffort":"high"},"system":"{{system}}","tools":"{{tools}}"},"reason":"initial"}}
{"type":"assistant/chunk","seq":5,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"reasoning-chunks","seq0":6,"time0":1784606400000,"data":{"turn":1,"step":1,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," user"," is"," asking"," about"," the"," current"," working"," directory","."," Let"," me"," check"," using"," p","wd","."]}}
{"type":"assistant/chunk","seq":23,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"block-start","index":1,"blockType":"tool-call"}}}
{"type":"tool-call-chunks","seq0":24,"time0":1784606400000,"data":{"turn":1,"step":1,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"id":"call_00_AqoWTncquNel5ZHsJHOo7491","name":"bash","args":["","{","\"","command","\"",": ","\"","p","wd","\"",", ","\"","description","\"",": ","\"","Print"," current"," working"," directory","\"","}"]}}
{"type":"assistant/chunk","seq":46,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The user is asking about the current working directory. Let me check using pwd."}}}}
{"type":"assistant/chunk","seq":47,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"block-end","index":1,"block":{"type":"tool-call","id":"call_00_AqoWTncquNel5ZHsJHOo7491","name":"bash","arguments":"{\"command\": \"pwd\", \"description\": \"Print current working directory\"}"}}}}
{"type":"assistant/chunk","seq":48,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"usage","usage":{"inputTokens":3332,"outputTokens":80,"cacheReadTokens":0,"reasoningTokens":17}}}}
{"type":"assistant/chunk","seq":49,"time":1784606400000,"data":{"turn":1,"step":1,"chunk":{"type":"finish","reason":{"kind":"tool-calls"}}}}
{"type":"assistant/message","seq":50,"time":1784606400000,"data":{"turn":1,"step":1,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The user is asking about the current working directory. Let me check using pwd."},{"type":"tool-call","id":"call_00_AqoWTncquNel5ZHsJHOo7491","name":"bash","arguments":"{\"command\": \"pwd\", \"description\": \"Print current working directory\"}"}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"063a9245-32c3-4551-9ace-d43f10ed5582"},"usage":{"inputTokens":3332,"outputTokens":80,"cacheReadTokens":0,"reasoningTokens":17}},"sourceEventSeqs":[5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49],"surfaceOp":"append"}
{"type":"tool/call","seq":51,"time":1784606400000,"data":{"turn":1,"step":1,"callId":"call_00_AqoWTncquNel5ZHsJHOo7491","name":"bash","arguments":"{\"command\": \"pwd\", \"description\": \"Print current working directory\"}"}}
{"type":"tool/result","seq":52,"time":1784606400000,"data":{"turn":1,"step":1,"message":{"source":{"kind":"tool","callId":"call_00_AqoWTncquNel5ZHsJHOo7491"},"content":[{"type":"tool-result","toolCallId":"call_00_AqoWTncquNel5ZHsJHOo7491","content":[{"type":"text","text":"{{cwd}}\n"}],"isError":false}],"role":"user","id":"16086d3b-6dfa-4970-a06e-78561475af8c"}},"sourceEventSeqs":[51],"surfaceOp":"append"}
{"type":"step/end","seq":53,"time":1784606400000,"data":{"turn":1,"step":1}}
{"type":"step/start","seq":54,"time":1784606400000,"data":{"turn":1,"step":2}}
{"type":"assistant/chunk","seq":55,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":0,"blockType":"reasoning"}}}
{"type":"reasoning-chunks","seq0":56,"time0":1784606400000,"data":{"turn":1,"step":2,"index":0,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," current"," working"," directory"," is"," `/","private","/t","mp","/d","sh","-t","ui","-s","n","apshot","-source","-check","out","-work","dir","-","61","I","wn","K","`."]}}
{"type":"assistant/chunk","seq":83,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"block-start","index":1,"blockType":"text"}}}
{"type":"text-chunks","seq0":84,"time0":1784606400000,"data":{"turn":1,"step":2,"index":1,"dt":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"texts":["The"," current"," working"," directory"," is",":\n\n","```\n","/","private","/t","mp","/d","sh","-t","ui","-s","n","apshot","-source","-check","out","-work","dir","-","61","I","wn","K","\n","```\n\n","Note"," that"," the"," Deep","Se","ek"," Har","ness"," SDK"," checkout"," is"," at"," `/","opt","/d","sh","-source","`"," —"," that","'s"," a"," separate"," location"," used"," for"," inspecting"," or"," extending"," D","SH"," itself",","," while"," the"," working"," directory"," above"," is"," the"," current"," working"," directory"," for"," this"," session","."]}}
{"type":"assistant/chunk","seq":161,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":0,"block":{"type":"reasoning","text":"The current working directory is `{{cwd}}`."}}}}
{"type":"assistant/chunk","seq":162,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"block-end","index":1,"block":{"type":"text","text":"The current working directory is:\n\n```\n{{cwd}}\n```\n\nNote that the DeepSeek Harness SDK checkout is at `/opt/dsh-source` — that's a separate location used for inspecting or extending DSH itself, while the working directory above is the current working directory for this session."}}}}
{"type":"assistant/chunk","seq":163,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"usage","usage":{"inputTokens":117,"outputTokens":105,"cacheReadTokens":3328,"reasoningTokens":27}}}}
{"type":"assistant/chunk","seq":164,"time":1784606400000,"data":{"turn":1,"step":2,"chunk":{"type":"finish","reason":{"kind":"stop"}}}}
{"type":"assistant/message","seq":165,"time":1784606400000,"data":{"turn":1,"step":2,"message":{"role":"assistant","content":[{"type":"reasoning","text":"The current working directory is `{{cwd}}`."},{"type":"text","text":"The current working directory is:\n\n```\n{{cwd}}\n```\n\nNote that the DeepSeek Harness SDK checkout is at `/opt/dsh-source` — that's a separate location used for inspecting or extending DSH itself, while the working directory above is the current working directory for this session."}],"source":{"kind":"model","provider":"deepseek","model":"deepseek-v4-flash"},"id":"76c18b85-00aa-45f6-a32f-a2bd75412470"},"usage":{"inputTokens":117,"outputTokens":105,"cacheReadTokens":3328,"reasoningTokens":27}},"sourceEventSeqs":[55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164],"surfaceOp":"append"}
{"type":"step/end","seq":166,"time":1784606400000,"data":{"turn":1,"step":2}}
{"type":"turn/end","seq":167,"time":1784606400000,"data":{"turn":1,"reason":{"kind":"completed"}}}

View File

@@ -0,0 +1,67 @@
terminal 100x36 buffer=normal length=36 base=0 viewport=0
lifecycle started=1 stopped=0 progress=inactive
title "what's the workdir? — DSH TUI snapshot"
cursor hidden column=7 viewportRow=32 bufferRow=32
buffer
0| " DEEPSEEK HARNESS"
style 1-8 fg=bright-magenta bold
style 10-16 bold
1| " what's the workdir?"
style 1-19 dim
2| " main-session"
style 1-12 dim
3| <blank>
4| "You "
style 0-2 fg=bright-magenta bold underline
5| "what's the workdir? "
6| <blank>
7| "Assistant "
style 0-8 fg=bright-magenta bold underline
8| "Reasoning "
style 0-8 dim italic
9| "The user is asking about the current working directory. Let me check using pwd. "
style 0-78 dim italic
10| <blank>
11| "● Tool / bash / Print current working directory"
style 0-46 fg=green
12| "$ pwd "
style 0-4 dim
13| "/workspace/project "
style 0-17 dim
14| "[exit 0] "
style 0-7 dim
15| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
style 0-46 dim
16| <blank>
17| "Assistant "
style 0-8 fg=bright-magenta bold underline
18| "Reasoning "
style 0-8 dim italic
19| "The current working directory is /workspace/project. "
style 0-32 dim italic
style 33-84 fg=cyan
style 85-85 dim italic
20| "The current working directory is: "
21| " "
22| " "
23| " /workspace/project "
style 2-53 fg=cyan
24| " "
25| " "
26| "Note that the DeepSeek Harness SDK checkout is at /opt/dsh-source — that's a separate location used "
style 50-64 fg=cyan
27| "for inspecting or extending DSH itself, while the working directory above is the current working "
28| "directory for this session. "
29| "Model wait 0.0s · Completed 2026-07-21 12:00:00 "
style 0-46 dim
30| <blank>
31| "/workspace/project deepseek-v4-flash ↑3.4k ↓185 cache 49% 3% c"
style 0-51 fg=bright-magenta bold
style 54-70 dim
style 73-93 dim
style 96-99 dim
32| " dsh ◍ "
style 1-3 fg=bright-magenta bold
style 5-6 dim
style 7-7 inverse
33-35| <blank>

View File

@@ -0,0 +1,76 @@
overlay 120x30 rows=20
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-119 dim
1| "│ ▄ DeepSeek Harness │"
style 0-0 dim
style 10-37 fg=blue
style 75-90 fg=blue bold
style 119-119 dim
2| "│ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │"
style 0-0 dim
style 9-38 fg=blue
style 119-119 dim
3| "│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段功能 │"
style 0-0 dim
style 4-43 fg=blue
style 119-119 dim
4| "│ ▄███████████████████▄ ████████████▀ 仍待完善,体验难免有些粗糙。 │"
style 0-0 dim
style 4-43 fg=blue
style 119-119 dim
5| "│ ▄██████████████████████▄ ▀█████████▀ │"
style 0-0 dim
style 4-42 fg=blue
style 119-119 dim
6| "│ ▄███▀█████████████████████▄ ████▀▀ “如切如磋,如琢如磨。” │"
style 0-0 dim
style 6-41 fg=blue
style 49-70 bold
style 119-119 dim
7| "│ ███ ▀▀█████████▀▀▀█████████▀ │"
style 0-0 dim
style 7-40 fg=blue
style 119-119 dim
8| "│ ███ ▀███████▀█ ▀███████ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的 │"
style 0-0 dim
style 7-39 fg=blue
style 119-119 dim
9| "│ ███▄ ▀███████▄ ▀█████▀ 问题,也可能促使我们重新审视,甚至推翻已有的设计。 │"
style 0-0 dim
style 7-39 fg=blue
style 119-119 dim
10| "│ ▀███ ▀██████████████ │"
style 0-0 dim
style 8-39 fg=blue
style 119-119 dim
11| "│ ▀███▄ ▀███████████▀ 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 │"
style 0-0 dim
style 8-38 fg=blue
style 119-119 dim
12| "│ ▀███▄ ▄▄▄ ▀████████▀ Session Log如需关闭请设置环境变量 DSH_TELEMETRY_DISABLED=1。另外 │"
style 0-0 dim
style 9-38 fg=blue
style 119-119 dim
13| "│ █████▄ ███▄▄ ▀█████▄▄ ,如果您有任何反馈与建议,请在企业微信群中留言告诉我们。每一条反馈, │"
style 0-0 dim
style 9-38 fg=blue
style 119-119 dim
14| "│ ▀█████████████▄▄▄▄█▀█████▀ 都会帮助我们把它打磨得更好。 │"
style 0-0 dim
style 8-39 fg=blue
style 119-119 dim
15| "│ ▀▀███████████▀▀ │"
style 0-0 dim
style 12-34 fg=blue
style 119-119 dim
16| "├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤"
style 0-119 dim
17| "│ Enter 继续 │"
style 0-0 dim
style 54-64 fg=bright-magenta bold
style 119-119 dim
18| "│ │"
style 0-0 dim
style 119-119 dim
19| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-119 dim

View File

@@ -0,0 +1,76 @@
overlay 160x30 rows=20
0| "╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮"
style 0-159 dim
1| "│ ▄ DeepSeek Harness │"
style 0-0 dim
style 10-37 fg=blue
style 95-110 fg=blue bold
style 159-159 dim
2| "│ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │"
style 0-0 dim
style 9-38 fg=blue
style 159-159 dim
3| "│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段功能仍待完善体验难免有些粗糙。 │"
style 0-0 dim
style 4-43 fg=blue
style 159-159 dim
4| "│ ▄███████████████████▄ ████████████▀ │"
style 0-0 dim
style 4-43 fg=blue
style 159-159 dim
5| "│ ▄██████████████████████▄ ▀█████████▀ “如切如磋,如琢如磨。” │"
style 0-0 dim
style 4-42 fg=blue
style 49-70 bold
style 159-159 dim
6| "│ ▄███▀█████████████████████▄ ████▀▀ │"
style 0-0 dim
style 6-41 fg=blue
style 159-159 dim
7| "│ ███ ▀▀█████████▀▀▀█████████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也可能促使我们重新审视,甚至推翻已 │"
style 0-0 dim
style 7-40 fg=blue
style 159-159 dim
8| "│ ███ ▀███████▀█ ▀███████ 有的设计。 │"
style 0-0 dim
style 7-39 fg=blue
style 159-159 dim
9| "│ ███▄ ▀███████▄ ▀█████▀ │"
style 0-0 dim
style 7-39 fg=blue
style 159-159 dim
10| "│ ▀███ ▀██████████████ 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session Log如需关闭请设置环境变量 │"
style 0-0 dim
style 8-39 fg=blue
style 159-159 dim
11| "│ ▀███▄ ▀███████████▀ DSH_TELEMETRY_DISABLED=1。另外如果您有任何反馈与建议请在企业微信群中留言告诉我们。每一条反馈都会帮助我 │"
style 0-0 dim
style 8-38 fg=blue
style 159-159 dim
12| "│ ▀███▄ ▄▄▄ ▀████████▀ 们把它打磨得更好。 │"
style 0-0 dim
style 9-38 fg=blue
style 159-159 dim
13| "│ █████▄ ███▄▄ ▀█████▄▄ │"
style 0-0 dim
style 9-38 fg=blue
style 159-159 dim
14| "│ ▀█████████████▄▄▄▄█▀█████▀ │"
style 0-0 dim
style 8-39 fg=blue
style 159-159 dim
15| "│ ▀▀███████████▀▀ │"
style 0-0 dim
style 12-34 fg=blue
style 159-159 dim
16| "├──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤"
style 0-159 dim
17| "│ Enter 继续 │"
style 0-0 dim
style 74-84 fg=bright-magenta bold
style 159-159 dim
18| "│ │"
style 0-0 dim
style 159-159 dim
19| "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
style 0-159 dim

View File

@@ -0,0 +1,31 @@
overlay 60x12 rows=10
0| "╭──────────────────────────────────────────────────────────╮"
style 0-59 dim
1| "│ DeepSeek Harness │"
style 0-0 dim
style 22-37 fg=blue bold
style 59-59 dim
2| "│ │"
style 0-0 dim
style 59-59 dim
3| "│ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部 │"
style 0-0 dim
style 59-59 dim
4| "│ 测试阶段,功能仍待完善,体验难免有些粗糙。 │"
style 0-0 dim
style 59-59 dim
5| "│ │"
style 0-0 dim
style 59-59 dim
6| "├──────────────────────────────────────────────────────────┤"
style 0-59 dim
7| "│ Enter 继续 │"
style 0-0 dim
style 24-34 fg=bright-magenta bold
style 59-59 dim
8| "│ ↑/↓ 滚动 ↓ │"
style 0-0 dim
style 24-35 dim
style 59-59 dim
9| "╰──────────────────────────────────────────────────────────╯"
style 0-59 dim

View File

@@ -0,0 +1,64 @@
overlay 60x30 rows=21
0| "╭──────────────────────────────────────────────────────────╮"
style 0-59 dim
1| "│ DeepSeek Harness │"
style 0-0 dim
style 22-37 fg=blue bold
style 59-59 dim
2| "│ │"
style 0-0 dim
style 59-59 dim
3| "│ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部 │"
style 0-0 dim
style 59-59 dim
4| "│ 测试阶段,功能仍待完善,体验难免有些粗糙。 │"
style 0-0 dim
style 59-59 dim
5| "│ │"
style 0-0 dim
style 59-59 dim
6| "│ “如切如磋,如琢如磨。” │"
style 0-0 dim
style 2-23 bold
style 59-59 dim
7| "│ │"
style 0-0 dim
style 59-59 dim
8| "│ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实 │"
style 0-0 dim
style 59-59 dim
9| "│ 使用中发现的问题,也可能促使我们重新审视,甚至推翻已有的 │"
style 0-0 dim
style 59-59 dim
10| "│ 设计。 │"
style 0-0 dim
style 59-59 dim
11| "│ │"
style 0-0 dim
style 59-59 dim
12| "│ 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认 │"
style 0-0 dim
style 59-59 dim
13| "│ 会上传所有 Session Log如需关闭请设置环境变量 │"
style 0-0 dim
style 59-59 dim
14| "│ DSH_TELEMETRY_DISABLED=1。另外如果您有任何反馈与建议 │"
style 0-0 dim
style 59-59 dim
15| "│ 请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把 │"
style 0-0 dim
style 59-59 dim
16| "│ 它打磨得更好。 │"
style 0-0 dim
style 59-59 dim
17| "├──────────────────────────────────────────────────────────┤"
style 0-59 dim
18| "│ Enter 继续 │"
style 0-0 dim
style 24-34 fg=bright-magenta bold
style 59-59 dim
19| "│ │"
style 0-0 dim
style 59-59 dim
20| "╰──────────────────────────────────────────────────────────╯"
style 0-59 dim

View File

@@ -0,0 +1,89 @@
overlay 80x30 rows=27
0| "╭──────────────────────────────────────────────────────────────────────────────╮"
style 0-79 dim
1| "│ ▄▄▄▄▄▄ ▄▄ │"
style 0-0 dim
style 33-46 fg=blue
style 79-79 dim
2| "│ ▄████████▄ ▀████▀ │"
style 0-0 dim
style 31-48 fg=blue
style 79-79 dim
3| "│ █▀▀▀▀███████▄██▀ │"
style 0-0 dim
style 32-47 fg=blue
style 79-79 dim
4| "│ █▄ ▀███ ▀███ │"
style 0-0 dim
style 32-46 fg=blue
style 79-79 dim
5| "│ ▀█▄ ▀█████ │"
style 0-0 dim
style 33-46 fg=blue
style 79-79 dim
6| "│ ▀█▄▄ █▄▄▀███▄ │"
style 0-0 dim
style 33-46 fg=blue
style 79-79 dim
7| "│ ▀▀▀▀▀▀ │"
style 0-0 dim
style 35-44 fg=blue
style 79-79 dim
8| "│ │"
style 0-0 dim
style 79-79 dim
9| "│ DeepSeek Harness │"
style 0-0 dim
style 32-47 fg=blue bold
style 79-79 dim
10| "│ │"
style 0-0 dim
style 79-79 dim
11| "│ 感谢您愿意拨冗试用 DeepSeek Harness。当前版本仍处于内部测试阶段功能仍待完 │"
style 0-0 dim
style 79-79 dim
12| "│ 善,体验难免有些粗糙。 │"
style 0-0 dim
style 79-79 dim
13| "│ │"
style 0-0 dim
style 79-79 dim
14| "│ “如切如磋,如琢如磨。” │"
style 0-0 dim
style 2-23 bold
style 79-79 dim
15| "│ │"
style 0-0 dim
style 79-79 dim
16| "│ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中发现的问题,也 │"
style 0-0 dim
style 79-79 dim
17| "│ 可能促使我们重新审视,甚至推翻已有的设计。 │"
style 0-0 dim
style 79-79 dim
18| "│ │"
style 0-0 dim
style 79-79 dim
19| "│ 为了帮助我们更准确地还原您真实使用中的问题,内测版本默认会上传所有 Session │"
style 0-0 dim
style 79-79 dim
20| "│ Log如需关闭请设置环境变量 DSH_TELEMETRY_DISABLED=1。另外如果您有任何反 │"
style 0-0 dim
style 79-79 dim
21| "│ 馈与建议,请在企业微信群中留言告诉我们。每一条反馈,都会帮助我们把它打磨得更 │"
style 0-0 dim
style 79-79 dim
22| "│ 好。 │"
style 0-0 dim
style 79-79 dim
23| "├──────────────────────────────────────────────────────────────────────────────┤"
style 0-79 dim
24| "│ Enter 继续 │"
style 0-0 dim
style 34-44 fg=bright-magenta bold
style 79-79 dim
25| "│ │"
style 0-0 dim
style 79-79 dim
26| "╰──────────────────────────────────────────────────────────────────────────────╯"
style 0-79 dim

View File

@@ -0,0 +1,346 @@
import { createHash } from 'node:crypto'
import { mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Context } from 'cordis'
import { visibleWidth } from '@earendil-works/pi-tui'
import {
type TuiOverlayHost,
type TuiOverlayRequest,
type TuiTheme,
} from '@deepseek-ai/dsh-tui'
import {
acknowledgeTuiFirstRunWelcome,
apply,
hasTuiFirstRunWelcomeAcknowledgement,
needsTuiFirstRunWelcomeAsciiArt,
TuiFirstRunWelcomeComponent,
tuiFirstRunWelcomeAcknowledgementPath,
tuiFirstRunWelcomeArtTier,
} from '../src/tui-onboarding/tui-first-run-welcome.ts'
import {
TUI_FIRST_RUN_WELCOME_NOTICE_COPY,
TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE,
TUI_FIRST_RUN_WELCOME_NOTICE_VERSION,
} from '../src/tui-onboarding/tui-first-run-welcome-copy.ts'
import { TUI_FIRST_RUN_WELCOME_WHALE } from '../src/tui-onboarding/tui-first-run-welcome-art.ts'
const mockDisposeRootAndExit = vi.hoisted(() => vi.fn())
vi.mock('@deepseek-ai/dsh-tui', async importOriginal => ({
...await importOriginal<typeof import('@deepseek-ai/dsh-tui')>(),
disposeRootAndExit: mockDisposeRootAndExit,
}))
const identityTheme: TuiTheme = Object.freeze({
text: (value: string) => value,
brand: (value: string) => value,
dim: (value: string) => value,
accent: (value: string) => value,
success: (value: string) => value,
warning: (value: string) => value,
error: (value: string) => value,
bold: (value: string) => value,
})
function hostFixture(rows: number): {
host: TuiOverlayHost
closed: () => boolean
invalidations: () => number
} {
let closed = false
let invalidations = 0
const controller = new AbortController()
return {
host: Object.freeze({
signal: controller.signal,
viewport: Object.freeze({ columns: 160, rows }),
theme: identityTheme,
display: (value: string) => value,
invalidate: () => { invalidations += 1 },
close: () => { closed = true },
}),
closed: () => closed,
invalidations: () => invalidations,
}
}
const copy = TUI_FIRST_RUN_WELCOME_NOTICE_COPY[TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE]
const openingSentence = `${copy.paragraphs[0]!.split('。', 1)[0]}`
const temporaryHomes: string[] = []
function artAnchor(tier: keyof typeof TUI_FIRST_RUN_WELCOME_WHALE): string {
return TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[tier === 'full' ? 2 : 0]!.trim()
}
function withoutWhitespace(value: string): string {
return value.replace(/\s/gu, '')
}
async function temporaryHome(prefix: string): Promise<string> {
const home = await mkdtemp(join(tmpdir(), prefix))
temporaryHomes.push(home)
return home
}
afterEach(async () => {
mockDisposeRootAndExit.mockClear()
await Promise.all(temporaryHomes.splice(0).map(home => rm(home, { recursive: true, force: true })))
})
describe('TUI first-run welcome acknowledgement', () => {
it('publishes one immutable per-version marker safely across concurrent acknowledgements', async () => {
const home = await temporaryHome('dsh-tui-welcome-ack-')
expect(await hasTuiFirstRunWelcomeAcknowledgement(home)).toBe(false)
await Promise.all(Array.from({ length: 8 }, () => acknowledgeTuiFirstRunWelcome(home)))
expect(await hasTuiFirstRunWelcomeAcknowledgement(home)).toBe(true)
const info = await stat(tuiFirstRunWelcomeAcknowledgementPath(home, TUI_FIRST_RUN_WELCOME_NOTICE_VERSION))
expect(info.isFile()).toBe(true)
if (process.platform !== 'win32') expect(info.mode & 0o777).toBe(0o600)
})
it('treats a notice-version bump as a new one-time acknowledgement', async () => {
const home = await temporaryHome('dsh-tui-welcome-version-')
await acknowledgeTuiFirstRunWelcome(home)
const nextVersion = TUI_FIRST_RUN_WELCOME_NOTICE_VERSION + 1
expect(await hasTuiFirstRunWelcomeAcknowledgement(home, nextVersion)).toBe(false)
await acknowledgeTuiFirstRunWelcome(home, nextVersion)
expect(await hasTuiFirstRunWelcomeAcknowledgement(home, nextVersion)).toBe(true)
})
it('rejects a malformed marker instead of silently acknowledging it', async () => {
const home = await temporaryHome('dsh-tui-welcome-malformed-')
await mkdir(tuiFirstRunWelcomeAcknowledgementPath(home, TUI_FIRST_RUN_WELCOME_NOTICE_VERSION), {
recursive: true,
})
await expect(hasTuiFirstRunWelcomeAcknowledgement(home)).rejects.toThrow('is not a file')
await expect(acknowledgeTuiFirstRunWelcome(home)).rejects.toThrow()
})
it('detects only explicit ASCII-only terminal environments', () => {
expect(needsTuiFirstRunWelcomeAsciiArt({ TERM: 'dumb' })).toBe(true)
expect(needsTuiFirstRunWelcomeAsciiArt({ LC_ALL: 'C' })).toBe(true)
expect(needsTuiFirstRunWelcomeAsciiArt({ LC_CTYPE: 'POSIX' })).toBe(true)
expect(needsTuiFirstRunWelcomeAsciiArt({ LANG: 'C' })).toBe(true)
expect(needsTuiFirstRunWelcomeAsciiArt({ LANG: 'en_US.UTF-8' })).toBe(false)
expect(typeof needsTuiFirstRunWelcomeAsciiArt()).toBe('boolean')
})
})
describe('TUI first-run welcome composition', () => {
it('pins the supplied official icon and exact Chinese copy at their owner boundaries', async () => {
const icon = (await readFile(new URL('../assets/deepseek-color.svg', import.meta.url), 'utf8')).trimEnd()
expect(createHash('sha256').update(icon).digest('hex'))
.toBe('deba5f98a5c1796e20fcac3149bcd7eb8a32f0bdd04d048819400b1f28bd1439')
expect(createHash('sha256').update(copy.paragraphs.join('\n')).digest('hex'))
.toBe('99f9a828b4f083b28de21bf5e03f939c00238531e765db78911957c44c6e98da')
expect(TUI_FIRST_RUN_WELCOME_NOTICE_COPY.en).toBe(copy)
})
it.each([
{ columns: 60, inner: 50, rows: 30, tier: undefined },
{ columns: 80, inner: 68, rows: 30, tier: 'minimal' },
{ columns: 100, inner: 84, rows: 34, tier: 'compact' },
{ columns: 120, inner: 104, rows: 30, tier: 'full' },
{ columns: 160, inner: 140, rows: 30, tier: 'full' },
] as const)('renders the responsive composition at $columns columns without overdraw', ({ inner, rows, tier }) => {
const fixture = hostFixture(rows)
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {}, () => {})
const renderWidth = inner + 4
const lines = component.render(renderWidth)
expect(tuiFirstRunWelcomeArtTier(inner, rows)).toBe(tier)
expect(lines.every(line => visibleWidth(line) <= renderWidth)).toBe(true)
if (tier === undefined) {
expect(lines.join('\n')).not.toMatch(/[]/u)
} else {
expect(lines.join('\n')).toContain(artAnchor(tier))
}
const rendered = lines.join('\n')
const optOut = copy.paragraphs.at(-1)!.match(/[A-Z_]+=1/u)![0]
expect(rendered).not.toContain(copy.scrollHint)
expect(rendered).toContain(copy.paragraphs.at(-1)!.match(/[A-Za-z]+ [A-Za-z]+/u)![0])
expect(rendered).toContain(optOut)
expect(lines.join('\n')).toContain(`Enter ${copy.continueLabel}`)
expect(lines.length).toBeLessThanOrEqual(Math.floor(rows * 0.9))
expect(lines.length).toBeGreaterThan(5)
})
it.each([
{ inner: 68, rows: 14, tier: undefined },
{ inner: 68, rows: 17, tier: undefined },
{ inner: 68, rows: 18, tier: 'minimal' },
{ inner: 84, rows: 21, tier: 'minimal' },
{ inner: 84, rows: 22, tier: 'compact' },
] as const)('degrades art to preserve the action at $rows rows', ({ inner, rows, tier }) => {
const fixture = hostFixture(rows)
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {}, () => {})
const lines = component.render(inner + 4)
expect(tuiFirstRunWelcomeArtTier(inner, rows)).toBe(tier)
expect(lines.length).toBeLessThanOrEqual(Math.floor(rows * 0.9))
expect(lines.join('\n')).toContain(`Enter ${copy.continueLabel}`)
})
it('drops the whale at low height while keeping prose, scrolling, and Enter reachable', () => {
const fixture = hostFixture(10)
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {}, () => {})
const initial = component.render(54).join('\n')
expect(tuiFirstRunWelcomeArtTier(50, 10)).toBeUndefined()
expect(initial).toContain(openingSentence)
expect(initial).toContain(`Enter ${copy.continueLabel}`)
component.handleInput('\x1b[F')
const end = component.render(54).join('\n')
expect(withoutWhitespace(end)).toContain(withoutWhitespace(copy.paragraphs.at(-1)!.slice(-7)))
expect(end).toContain(`Enter ${copy.continueLabel}`)
for (const key of ['\x1b[A', '\x1b[B', '\x1b[5~', '\x1b[6~', '\x1b[H', 'x']) {
component.handleInput(key)
}
component.invalidate()
})
it('renders a tiny viewport and a quotation-only paragraph without overdraw', () => {
const fixture = hostFixture(5)
const quoteOnly = { ...copy, paragraphs: ['“如切如磋,如琢如磨。”'] }
const component = new TuiFirstRunWelcomeComponent(fixture.host, quoteOnly, async () => {}, () => {})
const lines = component.render(2)
expect(lines.every(line => visibleWidth(line) <= 6)).toBe(true)
})
it('keeps the side-by-side composition aligned when prose outgrows the full raster', () => {
const fixture = hostFixture(40)
const longCopy = { ...copy, paragraphs: [copy.paragraphs.join(' ').repeat(4)] }
const component = new TuiFirstRunWelcomeComponent(fixture.host, longCopy, async () => {}, () => {})
const lines = component.render(100)
expect(lines.length).toBeGreaterThan(TUI_FIRST_RUN_WELCOME_WHALE.full.unicode.length)
expect(lines.every(line => visibleWidth(line) <= 100)).toBe(true)
component.handleInput('\x1b[F')
expect(component.render(100).join('\n')).toContain(copy.title)
})
it('renders the bit-equivalent ASCII icon fallback for an explicitly non-Unicode terminal', () => {
const fixture = hostFixture(30)
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {}, () => {}, true)
const rendered = component.render(72).join('\n')
expect(rendered).toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.ascii[0]!.trim())
expect(rendered).not.toMatch(/[]/u)
})
it.each(['full', 'compact', 'minimal'] as const)('keeps the $tier ASCII raster bit-equivalent', (tier) => {
const mapped = TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode.map(line => Array.from(line).map((cell) => {
if (cell === '▀') return "'"
if (cell === '▄') return '_'
if (cell === '█') return '#'
return cell
}).join(''))
expect(mapped).toEqual(TUI_FIRST_RUN_WELCOME_WHALE[tier].ascii)
})
it('ignores Escape and acknowledges only Enter before closing', async () => {
const fixture = hostFixture(30)
const acknowledge = vi.fn(async () => {})
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, acknowledge, () => {})
component.render(72)
component.handleInput('\x1b')
await Promise.resolve()
expect(acknowledge).not.toHaveBeenCalled()
expect(fixture.closed()).toBe(false)
component.handleInput('\r')
await vi.waitFor(() => { expect(fixture.closed()).toBe(true) })
expect(acknowledge).toHaveBeenCalledOnce()
})
it('keeps the notice eligible when Ctrl+C or Ctrl+D requests a normal exit', async () => {
const fixture = hostFixture(30)
const acknowledge = vi.fn(async () => {})
const exit = vi.fn()
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, acknowledge, exit)
component.handleInput('\x03')
component.handleInput('\x04')
expect(exit).toHaveBeenCalledTimes(2)
expect(acknowledge).not.toHaveBeenCalled()
expect(fixture.closed()).toBe(false)
})
it('does not start a second acknowledgement while the first Enter is pending', async () => {
const fixture = hostFixture(30)
const pending = Promise.withResolvers<undefined>()
const acknowledge = vi.fn(async () => pending.promise)
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, acknowledge, () => {})
component.render(72)
component.handleInput('\r')
component.handleInput('\r')
component.handleInput('\x1b[B')
expect(component.render(72).join('\n')).toContain(copy.saving)
expect(acknowledge).toHaveBeenCalledOnce()
pending.resolve(undefined)
await vi.waitFor(() => { expect(fixture.closed()).toBe(true) })
})
it('keeps the overlay open after a persistence failure and lets Enter retry', async () => {
const fixture = hostFixture(30)
let attempts = 0
const component = new TuiFirstRunWelcomeComponent(fixture.host, copy, async () => {
attempts += 1
if (attempts === 1) throw new Error('disk unavailable')
}, () => {})
component.render(72)
component.handleInput('\r')
await vi.waitFor(() => {
expect(component.render(72).join('\n')).toContain(copy.saveError)
})
expect(fixture.closed()).toBe(false)
component.handleInput('\r')
await vi.waitFor(() => { expect(fixture.closed()).toBe(true) })
expect(attempts).toBe(2)
expect(fixture.invalidations()).toBeGreaterThanOrEqual(3)
})
it('opens through the TUI extension and uses the launcher-owned acknowledgement closure', async () => {
const home = await temporaryHome('dsh-tui-welcome-apply-')
let request: TuiOverlayRequest | undefined
let disposePending: (() => Promise<void>) | undefined
const ctx = {
effect(register: () => () => Promise<void>) {
disposePending = register()
return () => {}
},
tui: {
openOverlay(value: TuiOverlayRequest) {
request = value
return {} as never
},
},
} as unknown as Context
apply(ctx, { dshHome: home })
expect(request?.options).toEqual({
width: '100%',
maxHeight: '90%',
anchor: 'center',
margin: 0,
})
const fixture = hostFixture(30)
const component = request?.create(fixture.host)
expect(component).toBeInstanceOf(TuiFirstRunWelcomeComponent)
component?.handleInput?.('\x03')
expect(mockDisposeRootAndExit).toHaveBeenCalledWith(ctx, 0)
component?.handleInput?.('\r')
await disposePending?.()
expect(await hasTuiFirstRunWelcomeAcknowledgement(home)).toBe(true)
apply(ctx, { dshHome: home, asciiArt: true })
expect(request?.create(fixture.host).render(72).join('\n'))
.toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.ascii[0]!.trim())
})
})

View File

@@ -1,6 +1,7 @@
import { createUserMessage, createMessage } from '@deepseek-ai/dsh-llm'
import { realpathSync } from 'node:fs'
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
@@ -8,12 +9,30 @@ import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
import { packChunkRuns, SessionId, type SessionEvent, type SessionHeader } from '@deepseek-ai/dsh-session'
import { logPath, toHeaderLine } from '../../../packages/session-persistence/session-persistence-jsonl/src/format.ts'
import { runTuiPtySmoke, type TuiPtySmokeOptions } from './pty-harness.ts'
import { HeadlessTerminal } from '../../../packages/ui/tui/tests/headless-terminal.ts'
import {
acknowledgeTuiFirstRunWelcome,
hasTuiFirstRunWelcomeAcknowledgement,
} from '../src/tui-onboarding/tui-first-run-welcome.ts'
import {
TUI_FIRST_RUN_WELCOME_NOTICE_COPY,
TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE,
} from '../src/tui-onboarding/tui-first-run-welcome-copy.ts'
import { TUI_FIRST_RUN_WELCOME_WHALE } from '../src/tui-onboarding/tui-first-run-welcome-art.ts'
const dshBinScript = fileURLToPath(new URL('../src/bin.ts', import.meta.url))
// `--config` layers an overlay over the shared base, so the default surface
// needs no config argument at all; these are the overlays under test.
const scriptedConfigPath = fileURLToPath(new URL('./fixtures/tui-scripted.cordis.yml', import.meta.url))
const tsconfigPath = fileURLToPath(new URL('../../../tsconfig.json', import.meta.url))
const firstRunSnapshots = fileURLToPath(new URL('./tui-first-run-snapshots/', import.meta.url))
const synchronizedFrameEnd = '\x1b[?2026l'
// Artifact mode gives the inner PTY driver 60 seconds and its execa owner a
// five-second backstop. Keep Vitest outside both deadlines so the harness can
// report its own marker, exit, and cleanup failure instead of being cut off.
const PTY_SMOKE_TEST_TIMEOUT_MS = process.env.DSH_EXAMPLE_MODE === 'lib'
? 75_000
: LOADER_SMOKE_TEST_TIMEOUT_MS
/**
* Seed the isolated process workspace: ordinary files land in `cwd`, personal
@@ -125,20 +144,77 @@ async function readLoggedRequestContext(cwd: string): Promise<LoggedRequestConte
* `tui.cordis.yml`, with no flags) or `configPath` (an overlay layered over that
* same base through `--config`).
*/
function smoke(overrides: Partial<TuiPtySmokeOptions> & { label: string }): Promise<string> {
function smoke(overrides: Partial<TuiPtySmokeOptions> & {
label: string
showFirstRunWelcome?: boolean
}): Promise<string> {
const { showFirstRunWelcome = false, prepare, ...options } = overrides
return runTuiPtySmoke({
tempDirPrefix: 'dsh-tui-smoke-',
binScript: dshBinScript,
tsconfigPath,
// Telemetry now mounts in the shared base: keep fixture sessions from
// POSTing to the production endpoint when run outside CI's workflow env.
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call', DSH_TELEMETRY_DISABLED: '1' },
env: {
DEEPSEEK_API_KEY: 'keyless-tui-no-call',
DSH_TELEMETRY_DISABLED: '1',
LANG: 'en_US.UTF-8',
LC_ALL: 'en_US.UTF-8',
LC_CTYPE: 'en_US.UTF-8',
TERM: 'xterm-256color',
},
// Artifact CI builds and smokes concurrently on a contended runner.
...(process.env.DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs: 60_000 } : {}),
...overrides,
...options,
prepare: async (cwd) => {
if (!showFirstRunWelcome) await acknowledgeTuiFirstRunWelcome(join(cwd, '.dsh'))
await prepare?.(cwd)
},
})
}
const firstRunCopy = TUI_FIRST_RUN_WELCOME_NOTICE_COPY[TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE]
const firstRunOpeningSentence = `${firstRunCopy.paragraphs[0]!.split('。', 1)[0]}`
function firstRunArtAnchor(tier: keyof typeof TUI_FIRST_RUN_WELCOME_WHALE): string {
return TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[tier === 'full' ? 2 : 0]!.trim()
}
/** Keep only the overlay rows, excluding platform-specific scrollback and the underlying TUI. */
function overlaySnapshot(snapshot: string, columns: number, rows: number): string {
const blocks: string[][] = []
for (const line of snapshot.split('\n')) {
if (/^\d+(?:-\d+)?~?\| /u.test(line)) blocks.push([line])
else if (line.startsWith(' style ') && blocks.length > 0) blocks.at(-1)?.push(line)
}
const first = blocks.findIndex(block => block[0]?.includes('╭') === true)
const last = blocks.findIndex((block, index) => index >= first && block[0]?.includes('╰') === true)
if (first < 0 || last < first) throw new Error('first-run PTY snapshot has no complete overlay frame')
const overlay = blocks.slice(first, last + 1).flatMap((block, index) => [
block[0]!.replace(/^\d+(?:-\d+)?(~)?\|/u, `${String(index)}$1|`),
...block.slice(1),
])
return [`overlay ${String(columns)}x${String(rows)} rows=${String(last - first + 1)}`, ...overlay, ''].join('\n')
}
/** Project the first synchronized PTY frame containing `marker` into an overlay-only snapshot. */
async function firstRunFrameSnapshot(
output: string,
marker: string,
columns: number,
rows: number,
): Promise<string> {
const markerIndex = output.indexOf(marker)
if (markerIndex < 0) throw new Error(`first-run PTY output has no marker ${JSON.stringify(marker)}`)
const frameEnd = output.indexOf(synchronizedFrameEnd, markerIndex)
if (frameEnd < 0) throw new Error(`first-run PTY output has no complete frame after ${JSON.stringify(marker)}`)
const terminal = new HeadlessTerminal(columns, rows)
try {
terminal.write(output.slice(0, frameEnd + synchronizedFrameEnd.length))
return overlaySnapshot(await terminal.snapshot(), columns, rows)
} finally {
await terminal.dispose()
}
}
// The scripted conversation switches to the pro model first: the scripted
// adapter proves routing + prompt variables by rejecting tool-ful calls on any
// other route (see fixtures/tui-scripted-llm.ts).
@@ -148,6 +224,138 @@ const SELECT_PRO_MODEL = [
] as const
describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
it.each([
{ columns: 60, tier: undefined },
{ columns: 80, tier: 'minimal' },
{ columns: 120, tier: 'full' },
{ columns: 160, tier: 'full' },
] as const)('renders and acknowledges the responsive first-run composition at $columns columns', async ({ columns, tier }) => {
const output = await smoke({
label: `dsh first-run welcome ${String(columns)} columns`,
tempDirPrefix: `dsh-tui-welcome-${String(columns)}-`,
configPath: scriptedConfigPath,
showFirstRunWelcome: true,
expectedExitCode: 0,
columns,
rows: 30,
actions: [
{
waitFor: `Enter ${firstRunCopy.continueLabel}`,
send: '\r\x03',
},
],
inspect: async (cwd) => {
expect(await hasTuiFirstRunWelcomeAcknowledgement(join(cwd, '.dsh'))).toBe(true)
const entries = await readdir(join(cwd, '.sessions'), { recursive: true })
const logs = entries.filter(name => name.endsWith('.jsonl'))
for (const log of logs) {
const stored = await readFile(join(cwd, '.sessions', log), 'utf8')
expect(stored).not.toContain(firstRunCopy.paragraphs[0])
}
},
})
await expect(await firstRunFrameSnapshot(output, firstRunOpeningSentence, columns, 30))
.toMatchFileSnapshot(join(firstRunSnapshots, `${String(columns)}-columns.expected.txt`))
if (tier === undefined) {
expect(output).not.toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.unicode[0]!.trim())
} else {
expect(output).toContain(firstRunArtAnchor(tier))
}
expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('keeps prose and Enter reachable in a low-height real PTY after dropping the whale', async () => {
const output = await smoke({
label: 'dsh low-height first-run welcome',
tempDirPrefix: 'dsh-tui-welcome-low-',
configPath: scriptedConfigPath,
showFirstRunWelcome: true,
expectedExitCode: 0,
columns: 60,
rows: 12,
actions: [
{ waitFor: firstRunOpeningSentence, send: '\x1b[F' },
{
waitFor: `Enter ${firstRunCopy.continueLabel}`,
occurrence: 2,
send: '\r\x03',
},
],
})
await expect(await firstRunFrameSnapshot(output, firstRunOpeningSentence, 60, 12))
.toMatchFileSnapshot(join(firstRunSnapshots, '60-columns-low-height.expected.txt'))
expect(output).toContain(firstRunCopy.title)
expect(output).toContain(firstRunOpeningSentence)
expect(output).toContain('企业微信群')
expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`)
expect(output).not.toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.unicode[0]!.trim())
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('shows once and skips the second launch under the same DSH_HOME', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'dsh-tui-welcome-twice-'))
try {
const first = await smoke({
label: 'dsh first welcome launch',
tempDirPrefix: 'unused-',
cwd,
configPath: scriptedConfigPath,
showFirstRunWelcome: true,
expectedExitCode: 0,
actions: [
{ waitFor: `Enter ${firstRunCopy.continueLabel}`, send: '\r\x03' },
],
})
expect(first).toContain(firstRunCopy.title)
const second = await smoke({
label: 'dsh second welcome launch',
tempDirPrefix: 'unused-',
cwd,
configPath: scriptedConfigPath,
showFirstRunWelcome: true,
expectedExitCode: process.platform === 'win32' ? 0 : -15,
actions: [{ waitFor: 'main-session-', signal: 'SIGTERM' }],
})
expect(second).not.toContain(firstRunOpeningSentence)
expect(second).not.toContain(`Enter ${firstRunCopy.continueLabel}`)
} finally {
await rm(cwd, { recursive: true, force: true })
}
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it.skipIf(process.platform === 'win32')('keeps the notice eligible when the process exits before Enter', async () => {
const cwd = await mkdtemp(join(tmpdir(), 'dsh-tui-welcome-abort-'))
try {
await smoke({
label: 'dsh aborted welcome launch',
tempDirPrefix: 'unused-',
cwd,
configPath: scriptedConfigPath,
showFirstRunWelcome: true,
expectedExitCode: -15,
actions: [{ waitFor: firstRunOpeningSentence, signal: 'SIGTERM' }],
inspect: async (workspace) => {
expect(await hasTuiFirstRunWelcomeAcknowledgement(join(workspace, '.dsh'))).toBe(false)
},
})
const next = await smoke({
label: 'dsh welcome after aborted launch',
tempDirPrefix: 'unused-',
cwd,
configPath: scriptedConfigPath,
showFirstRunWelcome: true,
expectedExitCode: 0,
actions: [
{ waitFor: `Enter ${firstRunCopy.continueLabel}`, send: '\r\x03' },
],
})
expect(next).toContain(firstRunOpeningSentence)
} finally {
await rm(cwd, { recursive: true, force: true })
}
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('boots pi-tui, sweeps the borderless banner in, enters plan mode, and restores the terminal', async () => {
// With no configured welcome the borderless banner sweeps in left-to-right;
// the detail line's session id (`main-session-<uuid>`) renders only once
@@ -170,7 +378,7 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
expect(output).not.toContain('╭')
expect(output).not.toContain('╮')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('switches models, streams a response, answers a user-question dialog, and exits cleanly', async () => {
const output = await smoke({
@@ -223,7 +431,7 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
expect(output).toContain('Registered tools')
expect(output).toContain('ask_user_question')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('loads a local skill via /skill: and delivers its body to the model as a user turn', async () => {
// The whole user-only invocation path in one keyless boot: `ctx.get('skills')`
@@ -259,7 +467,7 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
expect(output).not.toContain('[instructions]')
expect(output).toContain('Scripted skill body received.')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('adds a watched local skill to live /skill: autocomplete without restarting', async () => {
const skill = [
@@ -289,7 +497,7 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
})
expect(output).toContain('HOT_ADDED_COMPLETION_MARKER')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it.skipIf(process.env.DSH_EXAMPLE_MODE === 'lib')('fuzzy-completes an @file path without reading or submitting the file', async () => {
const output = await smoke({
@@ -314,11 +522,51 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
expect(output).toContain('File · terminal-special-case.t')
expect(output).toContain('@src/terminal-special-case.ts')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
})
describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
it('shows the terminal-local notice over a resumed session without changing its log', async () => {
let originalLineCount = 0
const output = await smoke({
label: 'dsh first-run notice on resume',
tempDirPrefix: 'dsh-tui-welcome-resume-',
binScript: dshBinScript,
configArgs: ['--resume', 'resume-target', '--config', scriptedConfigPath],
showFirstRunWelcome: true,
expectedExitCode: 0,
prepare: async (cwd) => {
await seedResumeSession(cwd)
const before = await readFile(logPath(
join(cwd, '.sessions'),
realpathSync.native(cwd),
SessionId('resume-target'),
'none',
), 'utf8')
originalLineCount = before.split('\n').filter(Boolean).length
},
actions: [
{ waitFor: `Enter ${firstRunCopy.continueLabel}`, send: '\r\x03' },
],
inspect: async (cwd) => {
const after = await readFile(logPath(
join(cwd, '.sessions'),
realpathSync.native(cwd),
SessionId('resume-target'),
'none',
), 'utf8')
expect(after).not.toContain(firstRunCopy.paragraphs[0])
const appended = after.split('\n').filter(Boolean).slice(originalLineCount)
.map(line => JSON.parse(line) as SessionEvent)
expect(appended).not.toContainEqual(expect.objectContaining({ type: 'user/message' }))
expect(appended).not.toContainEqual(expect.objectContaining({ type: 'turn/start' }))
},
})
expect(output).toContain(firstRunOpeningSentence)
expect(output).toContain('Resume selector design — DeepSeek Harness')
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('exec-replaces the TUI for /resume and restores the same session state', async () => {
const output = await smoke({
label: 'dsh in-place resume',
@@ -338,7 +586,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
expect(released).toBeGreaterThanOrEqual(0)
expect(restored).toBeGreaterThan(released)
expect(output).toContain('Preserve restored state')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('boots the shipped default config with no arguments and no personal overlay', async () => {
const output = await smoke({
@@ -353,7 +601,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
expect(output).not.toContain('╭')
expect(output).not.toContain('╮')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('applies the personal overlay: config.yaml patches an overlay-inserted row, the invoking directory\'s .env feeds its !!js, and the home .env stays out of the environment', async () => {
// The whole personal-config chain in one boot, plus the environment layer
@@ -390,7 +638,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
expect(output).toContain('PROJECT OVERLAY READY.')
expect(output).not.toContain('HOME ENV LEAKED.')
expect(output).toContain('\u001B[?2004l')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('fails loud instead of booting when the personal config.yaml is invalid', async () => {
const output = await smoke({
@@ -402,7 +650,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
expectedExitCode: 1,
})
expect(output).toContain('must be a top-level YAML array of loader patch entries')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('routes the --resume flag into the launcher session-identity slot, failing loud on a missing id', async () => {
// The flag path end to end: apps/cli parses `--resume missing-session`,
@@ -417,7 +665,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
expectedExitCode: 1,
})
expect(output).toContain('ui-tui: session "missing-session" failed to start:')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('prints the launcher-owned resume command on exit, naming the booted config', async () => {
// The exit line is built by apps/cli from this invocation, so it must carry
@@ -430,7 +678,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
actions: [{ waitFor: 'scripted TUI ready.', send: '/exit\r' }],
})
expect(output).toMatch(/To resume this session: dsh --resume=main-session-[0-9a-f-]{36} --config/)
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('keeps resume working when the personal overlay replaces the whole agent-loop config', async () => {
// Loader patches replace a targeted `config` key wholesale, so a personal
@@ -465,7 +713,7 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
actions: [{ waitFor: 'OVERLAY REPLACED THE CONFIG.', send: '/exit\r' }],
})
expect(output).toMatch(/To resume this session: dsh --resume=main-session-[0-9a-f-]{36}/)
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('reports a failing bash command exactly once, as the terminal card exit pill', async () => {
// The model-facing result ends in `[exit code: 3]`, which the terminal card
@@ -488,13 +736,14 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
expect(output).toContain('SCRIPTED_BASH_FAILED')
expect(output).toContain('[exit 3]')
expect(output).not.toContain('[exit code: 3]')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
it('tells the model its source path and offers the bundled maintenance skills', async () => {
it('distinguishes its source path from the current workdir and offers the bundled maintenance skills', async () => {
// The launcher resolves the checkout root three hops up from apps/cli/{src,lib};
// this test file sits an equal depth under the same root, so the same hop applies.
// The source-path line is a system-prompt section; the bundled skills reach the
// model through a durable user message, so each assertion targets its own field.
// The source-path line explicitly distinguishes that checkout from the current workdir;
// bundled skills reach the model through a durable user message, so each assertion
// targets its own field.
const sourceRoot = fileURLToPath(new URL('../../..', import.meta.url))
let context: LoggedRequestContext = { system: '', skillCatalog: '' }
await smoke({
@@ -510,9 +759,9 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
],
inspect: async (cwd) => { context = await readLoggedRequestContext(cwd) },
})
expect(context.system).toContain(`Your own source code is the checkout at ${sourceRoot}; you can read it there to learn how dsh works and how to extend it.`)
expect(context.system).toContain(`The DeepSeek Harness implementation checkout is at ${sourceRoot}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.`)
expect(context.skillCatalog).toContain("- `dsh-customize`: Customize or maintain any dsh source checkout — the one powering the current DSH process, the installed `dsh` command, or a sibling dsh/deepseek-harness clone. Use before any requested action that alters such a checkout's files or git state. Read-only questions that only inspect the checkout do not trigger this. Do not edit the personal staging checkout directly.")
expect(context.skillCatalog).toContain('- `dsh-upgrade`: Upgrades a source-installed, personally customized DSH checkout to upstream master while preserving local changes and an unchanged rollback worktree. Use when the user asks to update or upgrade DSH.')
expect(context.skillCatalog).toContain('- `dsh-upstream-customization`: Classifies personal DSH customizations for upstream contribution and, after explicit per-feature approval, rebuilds one on upstream master and opens a draft pull request. Use when the user asks to contribute, publish, or upstream a local DSH change, or asks whether one is worth proposing.')
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
}, PTY_SMOKE_TEST_TIMEOUT_MS)
})

View File

@@ -7,6 +7,7 @@ import { Context } from 'cordis'
import { scrubRequestHeaders, tokenizeSessionFixtureCwd } from '@deepseek-ai/dsh-acp-snapshot'
import type { Agent } from '@deepseek-ai/dsh-agent'
import * as AgentCore from '@deepseek-ai/dsh-agent-spine-demo'
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
import { LocalBashExecutor } from '@deepseek-ai/dsh-bash-local'
import LocalSubprocessService from '@deepseek-ai/dsh-subprocess-local'
import WorkerCodeRuntime from '@deepseek-ai/dsh-code-runtime-worker'
@@ -59,6 +60,10 @@ interface Scenario {
leavePlanModeAfterFirstTurn?: boolean
recorded: boolean
seedWorkspace?: boolean
/** Add the launcher's model-visible DSH source checkout at this fixed path. */
harnessSourceRoot?: string
/** Replace the real `pwd` result with a portable fixed-length workspace path. */
normalizePwdResult?: boolean
/**
* Load the opt-in `todo_write` tool for this scenario. The shipped TUI
* config omits it, so only the todo-plan scenario (the enabled-path proof)
@@ -115,6 +120,14 @@ const SCENARIOS: Scenario[] = [
expectedTools: ['bash'],
recorded: true,
},
{
name: 'source-checkout-workdir',
composition: 'native',
expectedTools: ['bash'],
recorded: true,
harnessSourceRoot: '/opt/dsh-source',
normalizePwdResult: true,
},
{
name: 'parallel-file-reads',
composition: 'native',
@@ -251,6 +264,12 @@ function rawSessionLog(session: Session): string {
].join('\n')
}
async function materializeFixtureCwd(fixtureFile: string, cwd: string, replayRoot: string): Promise<string> {
const realized = join(replayRoot, basename(fixtureFile))
await writeFile(realized, (await readFile(fixtureFile, 'utf8')).split('{{cwd}}').join(cwd))
return realized
}
function normalizeTerminalSnapshot(snapshot: string, cwd: string, displayCwd: string): string {
return snapshot
.split(`/private${cwd}`).join('/workspace/project')
@@ -294,6 +313,7 @@ async function mountScenarioContext(
displayCwd: string,
fixtureFile: string,
childFiles: string[],
replayRoot: string | undefined,
): Promise<Context> {
class SnapshotLocalFileSystem extends LocalFileSystem {
override async resolve(
@@ -313,6 +333,7 @@ async function mountScenarioContext(
tools: { mode: scenario.composition === 'code' ? 'code' : scenario.composition === 'advanced' ? 'both' : 'native' },
skills: { local: { agentsHome: join(cwd, '.agents') } },
})
if (scenario.harnessSourceRoot !== undefined) addHarnessSourceSection(ctx, scenario.harnessSourceRoot)
await ctx.plugin(TokenMeterService)
if (scenario.manualCompact === true) {
await ctx.plugin(DeferredSnapshotCompactService, { auto: false })
@@ -349,7 +370,12 @@ async function mountScenarioContext(
if (MODE === 'record' && scenario.recorded) {
await ctx.plugin(LlmDeepSeek)
} else {
installLlmReplay(ctx, { file: fixtureFile, childFiles, providers: PROVIDERS })
if (replayRoot === undefined) throw new Error('replay mode requires an isolated fixture directory')
// Recorded model text may name the generated cwd. Realize the portable token
// outside that cwd so tools see only the scenario workspace during replay.
const replayFile = await materializeFixtureCwd(fixtureFile, cwd, replayRoot)
const replayChildFiles = await Promise.all(childFiles.map(file => materializeFixtureCwd(file, cwd, replayRoot)))
installLlmReplay(ctx, { file: replayFile, childFiles: replayChildFiles, providers: PROVIDERS })
}
return ctx
}
@@ -373,15 +399,27 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
const cwd = await mkdtemp(join(SNAPSHOT_TMP_ROOT, `dsh-tui-snapshot-${scenario.name}-`))
const displayCwd = `/tmp/${basename(cwd)}`
let replayRoot: string | undefined
let ctx: Context | undefined
let controller: ReturnType<typeof createTuiChat> | undefined
const terminal = new HeadlessTerminal(100, 36)
try {
if (!(MODE === 'record' && scenario.recorded)) {
replayRoot = await mkdtemp(join(SNAPSHOT_TMP_ROOT, `dsh-tui-replay-${scenario.name}-`))
}
if (scenario.seedWorkspace === true) {
const source = join(fixtureDir(scenario), 'workspace')
await cp(source, cwd, { recursive: true })
}
ctx = await mountScenarioContext(scenario, cwd, displayCwd, fixtureFile, childFiles)
ctx = await mountScenarioContext(scenario, cwd, displayCwd, fixtureFile, childFiles, replayRoot)
if (scenario.normalizePwdResult === true) {
ctx.on('tools/post-execute', async (exec, result, next) => {
const args = exec.arguments as { command?: unknown }
return exec.name === 'bash' && args.command === 'pwd' && !result.isError
? { kind: 'accept', content: [{ type: 'text', text: '/workspace/project\n' }] }
: next()
})
}
const disposedSessions: Session[] = []
ctx.on('session/disposed', (session) => { disposedSessions.push(session) })
const workflowEvents: string[] = []
@@ -582,6 +620,10 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
const firstHeader = events.find(event => event.type === 'request/header')
expect(firstHeader?.type === 'request/header' && firstHeader.data.header.system)
.toContain(FILE_REFERENCE_PROMPT)
if (scenario.harnessSourceRoot !== undefined) {
expect(firstHeader?.type === 'request/header' && firstHeader.data.header.system)
.toContain(`The DeepSeek Harness implementation checkout is at ${scenario.harnessSourceRoot}. The checkout location and current working directory are separate values and may differ; never infer the working directory from this path. Use pwd to determine the current working directory. Use this checkout only to inspect or extend DSH itself.`)
}
expect(events.filter(event => event.type === 'tool/call').map(event => event.data.name)).toEqual(scenario.expectedTools)
for (const [type, count] of Object.entries(scenario.expectedEventCounts ?? {})) {
expect(events.filter(event => event.type === type), `${scenario.name} must emit ${type}`).toHaveLength(count)
@@ -734,6 +776,7 @@ async function runScenario(scenario: Scenario): Promise<ScenarioResult> {
await ctx?.fiber.dispose()
await terminal.dispose()
await rm(cwd, { recursive: true, force: true })
if (replayRoot !== undefined) await rm(replayRoot, { recursive: true, force: true })
clock.mockRestore()
}
}