feat(tui): add versioned first-run welcome
This commit is contained in:
@@ -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,8 +37,13 @@ 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 action.get("delayMs", 0) > 0:
|
||||
time.sleep(action["delayMs"] / 1000)
|
||||
if "writeFile" in action:
|
||||
target = os.path.join(cwd, action["writeFile"]["path"])
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
@@ -68,11 +74,13 @@ 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 delayMs?: number }
|
||||
| {
|
||||
readonly waitFor: string
|
||||
readonly occurrence?: number
|
||||
readonly writeFile: { readonly path: string; readonly content: string }
|
||||
readonly send?: string
|
||||
readonly delayMs?: number
|
||||
}
|
||||
|
||||
/** Inputs for a keyless real-Loader TUI process smoke. */
|
||||
@@ -89,6 +97,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 +133,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 +166,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 +175,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,16 +185,23 @@ 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) {
|
||||
const target = join(cwd, action.writeFile.path)
|
||||
mkdirSync(dirname(target), { recursive: true })
|
||||
writeFileSync(target, action.writeFile.content)
|
||||
const input = action.send
|
||||
if (input !== undefined) terminal.write(input)
|
||||
if (input !== undefined) {
|
||||
if (action.delayMs === undefined) terminal.write(input)
|
||||
else setTimeout(() => { terminal.write(input) }, action.delayMs)
|
||||
}
|
||||
} else {
|
||||
terminal.write(action.send)
|
||||
if (action.delayMs === undefined) terminal.write(action.send)
|
||||
else setTimeout(() => { terminal.write(action.send) }, action.delayMs)
|
||||
}
|
||||
actionIndex += 1
|
||||
}
|
||||
@@ -205,7 +228,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 +255,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 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
terminal 120x30 buffer=normal length=32 base=2 viewport=2
|
||||
lifecycle started=0 stopped=0 progress=inactive
|
||||
title ""
|
||||
cursor visible column=0 viewportRow=29 bufferRow=31
|
||||
viewport
|
||||
2| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
3| " ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ "
|
||||
style 1-118 dim
|
||||
4| " │ ▄ DeepSeek Harness │ "
|
||||
style 1-1 dim
|
||||
style 11-38 fg=blue
|
||||
style 75-90 fg=blue bold
|
||||
style 118-118 dim
|
||||
5| " │ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │ "
|
||||
style 1-1 dim
|
||||
style 10-39 fg=blue
|
||||
style 118-118 dim
|
||||
6| "/│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │e"
|
||||
style 0-0 fg=bright-magenta bold
|
||||
style 1-1 dim
|
||||
style 5-44 fg=blue
|
||||
style 118-119 dim
|
||||
7| " │ ▄███████████████████▄ ████████████▀ │ "
|
||||
style 1-1 dim
|
||||
style 5-44 fg=blue
|
||||
style 118-118 dim
|
||||
8| " │ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ "
|
||||
style 1-1 dim
|
||||
style 5-43 fg=blue
|
||||
style 118-118 dim
|
||||
9| " │ ▄███▀█████████████████████▄ ████▀▀ │ "
|
||||
style 1-1 dim
|
||||
style 7-42 fg=blue
|
||||
style 118-118 dim
|
||||
10| " │ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │ "
|
||||
style 1-1 dim
|
||||
style 8-41 fg=blue
|
||||
style 50-71 bold
|
||||
style 118-118 dim
|
||||
11| " │ ███ ▀███████▀█ ▀███████ │ "
|
||||
style 1-1 dim
|
||||
style 8-40 fg=blue
|
||||
style 118-118 dim
|
||||
12| " │ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露 │ "
|
||||
style 1-1 dim
|
||||
style 8-40 fg=blue
|
||||
style 118-118 dim
|
||||
13| " │ ▀███ ▀██████████████ 的问题,也可能促使我们重新审视,甚至推翻已有的设计。 │ "
|
||||
style 1-1 dim
|
||||
style 9-40 fg=blue
|
||||
style 118-118 dim
|
||||
14| " │ ▀███▄ ▀███████████▀ │ "
|
||||
style 1-1 dim
|
||||
style 9-39 fg=blue
|
||||
style 118-118 dim
|
||||
15| " │ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚 │ "
|
||||
style 1-1 dim
|
||||
style 10-39 fg=blue
|
||||
style 118-118 dim
|
||||
16| " │ █████▄ ███▄▄ ▀█████▄▄ 至反而为工作平添了麻烦,请在企业微信群中留言,将使用感受告诉我们。 │ "
|
||||
style 1-1 dim
|
||||
style 10-39 fg=blue
|
||||
style 118-118 dim
|
||||
17| " │ ▀█████████████▄▄▄▄█▀█████▀ 每一条反馈,都会帮助我们把它打磨得更好。 │ "
|
||||
style 1-1 dim
|
||||
style 9-40 fg=blue
|
||||
style 118-118 dim
|
||||
18| " │ ▀▀███████████▀▀ │ "
|
||||
style 1-1 dim
|
||||
style 13-35 fg=blue
|
||||
style 118-118 dim
|
||||
19| " │ │ "
|
||||
style 1-1 dim
|
||||
style 118-118 dim
|
||||
20| " │ │ "
|
||||
style 1-1 dim
|
||||
style 118-118 dim
|
||||
21| " │ │ "
|
||||
style 1-1 dim
|
||||
style 118-118 dim
|
||||
22| " │ │ "
|
||||
style 1-1 dim
|
||||
style 118-118 dim
|
||||
23| " │ │ "
|
||||
style 1-1 dim
|
||||
style 118-118 dim
|
||||
24| " │ │ "
|
||||
style 1-1 dim
|
||||
style 118-118 dim
|
||||
25| " │ │ "
|
||||
style 1-1 dim
|
||||
style 118-118 dim
|
||||
26| " ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ "
|
||||
style 1-118 dim
|
||||
27| " │ Enter 继续 │ "
|
||||
style 1-1 dim
|
||||
style 54-64 fg=bright-magenta bold
|
||||
style 118-118 dim
|
||||
28| " │ │ "
|
||||
style 1-1 dim
|
||||
style 118-118 dim
|
||||
29| " ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
|
||||
style 1-118 dim
|
||||
30-31| <blank>
|
||||
@@ -0,0 +1,106 @@
|
||||
terminal 160x30 buffer=normal length=32 base=2 viewport=2
|
||||
lifecycle started=0 stopped=0 progress=inactive
|
||||
title ""
|
||||
cursor visible column=0 viewportRow=29 bufferRow=31
|
||||
viewport
|
||||
2| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
3| " ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ "
|
||||
style 1-158 dim
|
||||
4| " │ ▄ DeepSeek Harness │ "
|
||||
style 1-1 dim
|
||||
style 11-38 fg=blue
|
||||
style 95-110 fg=blue bold
|
||||
style 158-158 dim
|
||||
5| " │ ▄▄▄▄▄▄▄▄▄▄███▀ ██▄ │ "
|
||||
style 1-1 dim
|
||||
style 10-39 fg=blue
|
||||
style 158-158 dim
|
||||
6| "/│ ▄███████████████▄ ████▄ ▄▄▄▄██ 感谢您愿意拨冗试用 DeepSeek Harness。 │ "
|
||||
style 0-0 fg=bright-magenta bold
|
||||
style 1-1 dim
|
||||
style 5-44 fg=blue
|
||||
style 158-158 dim
|
||||
7| " │ ▄███████████████████▄ ████████████▀ │ "
|
||||
style 1-1 dim
|
||||
style 5-44 fg=blue
|
||||
style 158-158 dim
|
||||
8| " │ ▄██████████████████████▄ ▀█████████▀ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ "
|
||||
style 1-1 dim
|
||||
style 5-43 fg=blue
|
||||
style 158-158 dim
|
||||
9| " │ ▄███▀█████████████████████▄ ████▀▀ │ "
|
||||
style 1-1 dim
|
||||
style 7-42 fg=blue
|
||||
style 158-158 dim
|
||||
10| " │ ███ ▀▀█████████▀▀▀█████████▀ “如切如磋,如琢如磨。” │ "
|
||||
style 1-1 dim
|
||||
style 8-41 fg=blue
|
||||
style 50-71 bold
|
||||
style 158-158 dim
|
||||
11| " │ ███ ▀███████▀█ ▀███████ │ "
|
||||
style 1-1 dim
|
||||
style 8-40 fg=blue
|
||||
style 158-158 dim
|
||||
12| " │ ███▄ ▀███████▄ ▀█████▀ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题,也可能促使我们重新审视,甚至推翻 │ "
|
||||
style 1-1 dim
|
||||
style 8-40 fg=blue
|
||||
style 158-158 dim
|
||||
13| " │ ▀███ ▀██████████████ 已有的设计。 │ "
|
||||
style 1-1 dim
|
||||
style 9-40 fg=blue
|
||||
style 158-158 dim
|
||||
14| " │ ▀███▄ ▀███████████▀ │ "
|
||||
style 1-1 dim
|
||||
style 9-39 fg=blue
|
||||
style 158-158 dim
|
||||
15| " │ ▀███▄ ▄▄▄ ▀████████▀ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未能帮到您,甚至反而为工作平添了麻烦,请在企业微信群中 │ "
|
||||
style 1-1 dim
|
||||
style 10-39 fg=blue
|
||||
style 158-158 dim
|
||||
16| " │ █████▄ ███▄▄ ▀█████▄▄ 留言,将使用感受告诉我们。每一条反馈,都会帮助我们把它打磨得更好。 │ "
|
||||
style 1-1 dim
|
||||
style 10-39 fg=blue
|
||||
style 158-158 dim
|
||||
17| " │ ▀█████████████▄▄▄▄█▀█████▀ │ "
|
||||
style 1-1 dim
|
||||
style 9-40 fg=blue
|
||||
style 158-158 dim
|
||||
18| " │ ▀▀███████████▀▀ │ "
|
||||
style 1-1 dim
|
||||
style 13-35 fg=blue
|
||||
style 158-158 dim
|
||||
19| " │ │ "
|
||||
style 1-1 dim
|
||||
style 158-158 dim
|
||||
20| " │ │ "
|
||||
style 1-1 dim
|
||||
style 158-158 dim
|
||||
21| " │ │ "
|
||||
style 1-1 dim
|
||||
style 158-158 dim
|
||||
22| " │ │ "
|
||||
style 1-1 dim
|
||||
style 158-158 dim
|
||||
23| " │ │ "
|
||||
style 1-1 dim
|
||||
style 158-158 dim
|
||||
24| " │ │ "
|
||||
style 1-1 dim
|
||||
style 158-158 dim
|
||||
25| " │ │ "
|
||||
style 1-1 dim
|
||||
style 158-158 dim
|
||||
26| " ├────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┤ "
|
||||
style 1-158 dim
|
||||
27| " │ Enter 继续 │ "
|
||||
style 1-1 dim
|
||||
style 74-84 fg=bright-magenta bold
|
||||
style 158-158 dim
|
||||
28| " │ │ "
|
||||
style 1-1 dim
|
||||
style 158-158 dim
|
||||
29| " ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ "
|
||||
style 1-158 dim
|
||||
30-31| <blank>
|
||||
@@ -0,0 +1,100 @@
|
||||
terminal 60x30 buffer=normal length=34 base=4 viewport=4
|
||||
lifecycle started=0 stopped=0 progress=inactive
|
||||
title ""
|
||||
cursor visible column=0 viewportRow=29 bufferRow=33
|
||||
viewport
|
||||
4| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
5| " ╭────────────────────────────────────────────────────────╮ "
|
||||
style 1-58 dim
|
||||
6| " │ ▄▄▄▄▄▄ ▄▄ │ "
|
||||
style 1-1 dim
|
||||
style 23-36 fg=blue
|
||||
style 58-58 dim
|
||||
7| " │ ▄████████▄ ▀████▀ │ "
|
||||
style 1-1 dim
|
||||
style 21-38 fg=blue
|
||||
style 58-58 dim
|
||||
8| "/│ █▀▀▀▀███████▄██▀ │h"
|
||||
style 0-0 fg=bright-magenta bold
|
||||
style 1-1 dim
|
||||
style 22-37 fg=blue
|
||||
style 58-58 dim
|
||||
style 59-59 fg=bright-magenta bold
|
||||
9| " │ █▄ ▀███ ▀███ │ "
|
||||
style 1-1 dim
|
||||
style 22-36 fg=blue
|
||||
style 58-58 dim
|
||||
10| " │ ▀█▄ ▀█████ │ "
|
||||
style 1-1 dim
|
||||
style 23-36 fg=blue
|
||||
style 58-58 dim
|
||||
11| " │ ▀█▄▄ █▄▄▀███▄ │ "
|
||||
style 1-1 dim
|
||||
style 23-36 fg=blue
|
||||
style 58-58 dim
|
||||
12| " │ ▀▀▀▀▀▀ │ "
|
||||
style 1-1 dim
|
||||
style 25-34 fg=blue
|
||||
style 58-58 dim
|
||||
13| " │ │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
14| " │ DeepSeek Harness │ "
|
||||
style 1-1 dim
|
||||
style 22-37 fg=blue bold
|
||||
style 58-58 dim
|
||||
15| " │ │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
16| " │ 感谢您愿意拨冗试用 DeepSeek Harness。 │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
17| " │ │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
18| " │ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体 │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
19| " │ 验难免粗粝。 │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
20| " │ │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
21| " │ “如切如磋,如琢如磨。” │ "
|
||||
style 1-1 dim
|
||||
style 3-24 bold
|
||||
style 58-58 dim
|
||||
22| " │ │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
23| " │ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真 │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
24| " │ 实使用中暴露的问题,也可能促使我们重新审视,甚至推翻已 │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
25| " │ 有的设计。 │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
26| " │ │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
27| " │ 我们尤其希望听见那些失败、困惑与不顺手的时刻——如果它未 │ "
|
||||
style 1-1 dim
|
||||
style 58-58 dim
|
||||
28| " ├────────────────────────────────────────────────────────┤ "
|
||||
style 1-58 dim
|
||||
29| " │ Enter 继续 │ "
|
||||
style 1-1 dim
|
||||
style 24-34 fg=bright-magenta bold
|
||||
style 58-58 dim
|
||||
30| " │ ↑/↓ 滚动 ↓ │ "
|
||||
style 1-1 dim
|
||||
style 24-35 dim
|
||||
style 58-58 dim
|
||||
31| " ╰────────────────────────────────────────────────────────╯ "
|
||||
style 1-58 dim
|
||||
32-33| <blank>
|
||||
@@ -0,0 +1,103 @@
|
||||
terminal 80x30 buffer=normal length=33 base=3 viewport=3
|
||||
lifecycle started=0 stopped=0 progress=inactive
|
||||
title ""
|
||||
cursor visible column=0 viewportRow=29 bufferRow=32
|
||||
viewport
|
||||
3| " DEEPSEEK HARNESS"
|
||||
style 1-8 fg=bright-magenta bold
|
||||
style 10-16 bold
|
||||
4| " ╭────────────────────────────────────────────────────────────────────────────╮ "
|
||||
style 1-78 dim
|
||||
5| " │ ▄▄▄▄▄▄▄██▀ █▄ ▄ │ "
|
||||
style 1-1 dim
|
||||
style 26-53 fg=blue
|
||||
style 78-78 dim
|
||||
6| " │ ▄███████████▄▄ ███▄▄████ │ "
|
||||
style 1-1 dim
|
||||
style 26-53 fg=blue
|
||||
style 78-78 dim
|
||||
7| "/│ ████████████████▄ ▀██████▀ │F"
|
||||
style 0-0 fg=bright-magenta bold
|
||||
style 1-1 dim
|
||||
style 26-52 fg=blue
|
||||
style 78-78 dim
|
||||
style 79-79 fg=bright-magenta bold
|
||||
8| " │ ██▀▀▀▀▀████████████▄▄██▀ │ "
|
||||
style 1-1 dim
|
||||
style 28-51 fg=blue
|
||||
style 78-78 dim
|
||||
9| " │ ██ ▀█████▄ ▀█████ │ "
|
||||
style 1-1 dim
|
||||
style 28-50 fg=blue
|
||||
style 78-78 dim
|
||||
10| " │ ██▄ ▀████▄ ▄████ │ "
|
||||
style 1-1 dim
|
||||
style 28-50 fg=blue
|
||||
style 78-78 dim
|
||||
11| " │ ██▄ ████████▀ │ "
|
||||
style 1-1 dim
|
||||
style 29-50 fg=blue
|
||||
style 78-78 dim
|
||||
12| " │ ██▄ ▄▄ ▀█████▀ │ "
|
||||
style 1-1 dim
|
||||
style 29-49 fg=blue
|
||||
style 78-78 dim
|
||||
13| " │ ▀███▄▄▄███▄ ████▄▄ │ "
|
||||
style 1-1 dim
|
||||
style 29-50 fg=blue
|
||||
style 78-78 dim
|
||||
14| " │ ▀▀▀███████▀▀ │ "
|
||||
style 1-1 dim
|
||||
style 31-47 fg=blue
|
||||
style 78-78 dim
|
||||
15| " │ │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
16| " │ DeepSeek Harness │ "
|
||||
style 1-1 dim
|
||||
style 32-47 fg=blue bold
|
||||
style 78-78 dim
|
||||
17| " │ │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
18| " │ 感谢您愿意拨冗试用 DeepSeek Harness。 │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
19| " │ │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
20| " │ 目前的版本仍处于内部测试阶段,有些功能仍待完善,有些体验难免粗粝。 │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
21| " │ │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
22| " │ “如切如磋,如琢如磨。” │ "
|
||||
style 1-1 dim
|
||||
style 3-24 bold
|
||||
style 78-78 dim
|
||||
23| " │ │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
24| " │ 产品的成长,离不开一次次真实的碰撞与坦诚的反馈。您在真实使用中暴露的问题, │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
25| " │ 也可能促使我们重新审视,甚至推翻已有的设计。 │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
26| " │ │ "
|
||||
style 1-1 dim
|
||||
style 78-78 dim
|
||||
27| " ├────────────────────────────────────────────────────────────────────────────┤ "
|
||||
style 1-78 dim
|
||||
28| " │ Enter 继续 │ "
|
||||
style 1-1 dim
|
||||
style 34-44 fg=bright-magenta bold
|
||||
style 78-78 dim
|
||||
29| " │ ↑/↓ 滚动 ↓ │ "
|
||||
style 1-1 dim
|
||||
style 34-45 dim
|
||||
style 78-78 dim
|
||||
30| " ╰────────────────────────────────────────────────────────────────────────────╯ "
|
||||
style 1-78 dim
|
||||
31-32| <blank>
|
||||
189
apps/cli/tests/tui-first-run-welcome.spec.ts
Normal file
189
apps/cli/tests/tui-first-run-welcome.spec.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
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 { visibleWidth } from '@earendil-works/pi-tui'
|
||||
import type { TuiOverlayHost, TuiTheme } from '@deepseek-ai/dsh-tui'
|
||||
import {
|
||||
acknowledgeTuiFirstRunWelcome,
|
||||
hasTuiFirstRunWelcomeAcknowledgement,
|
||||
TuiFirstRunWelcomeComponent,
|
||||
tuiFirstRunWelcomeAcknowledgementPath,
|
||||
tuiFirstRunWelcomeArtTier,
|
||||
} from '../src/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-first-run-welcome-copy.ts'
|
||||
import { TUI_FIRST_RUN_WELCOME_WHALE } from '../src/tui-first-run-welcome-art.ts'
|
||||
|
||||
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 temporaryHomes: string[] = []
|
||||
|
||||
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 () => {
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
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('c75e395999f572ee231688ef70d5b7f553de3809b57ce8160b4406bd7650f2ec')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ columns: 60, inner: 50, rows: 30, tier: 'minimal' },
|
||||
{ columns: 80, inner: 68, rows: 30, tier: 'compact' },
|
||||
{ columns: 120, inner: 104, rows: 30, tier: 'full' },
|
||||
{ columns: 160, inner: 140, rows: 30, tier: 'full' },
|
||||
] as const)('renders the $tier 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)
|
||||
expect(lines.join('\n')).toContain(TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[0]!.trim())
|
||||
expect(lines.join('\n')).toContain(`Enter ${copy.continueLabel}`)
|
||||
expect(lines).toHaveLength(Math.floor(rows * 0.9))
|
||||
})
|
||||
|
||||
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(copy.paragraphs[0])
|
||||
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(-10)))
|
||||
expect(end).toContain(`Enter ${copy.continueLabel}`)
|
||||
})
|
||||
|
||||
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.compact.ascii[0]!.trim())
|
||||
expect(rendered).not.toMatch(/[▀▄█]/u)
|
||||
})
|
||||
|
||||
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 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)
|
||||
})
|
||||
})
|
||||
@@ -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,24 @@ 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-first-run-welcome.ts'
|
||||
import {
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_COPY,
|
||||
TUI_FIRST_RUN_WELCOME_NOTICE_LOCALE,
|
||||
} from '../src/tui-first-run-welcome-copy.ts'
|
||||
import { TUI_FIRST_RUN_WELCOME_WHALE } from '../src/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('./snapshots/tui-first-run-welcome/', import.meta.url))
|
||||
const synchronizedFrameEnd = '\x1b[?2026l'
|
||||
|
||||
/**
|
||||
* Seed the isolated process workspace: ordinary files land in `cwd`, personal
|
||||
@@ -125,18 +138,54 @@ 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,
|
||||
env: { DEEPSEEK_API_KEY: 'keyless-tui-no-call' },
|
||||
env: {
|
||||
DEEPSEEK_API_KEY: 'keyless-tui-no-call',
|
||||
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]
|
||||
|
||||
/** Project the first synchronized PTY frame containing `marker` into the stable terminal snapshot format. */
|
||||
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 await terminal.snapshot()
|
||||
} 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).
|
||||
@@ -146,6 +195,94 @@ const SELECT_PRO_MODEL = [
|
||||
] as const
|
||||
|
||||
describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
|
||||
it.each([
|
||||
{ columns: 60, tier: 'minimal' },
|
||||
{ columns: 80, tier: 'compact' },
|
||||
{ columns: 120, tier: 'full' },
|
||||
{ columns: 160, tier: 'full' },
|
||||
] as const)('renders and acknowledges the $tier 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,
|
||||
columns,
|
||||
rows: 30,
|
||||
actions: [
|
||||
{ waitFor: firstRunCopy.paragraphs[0]!, send: '\r' },
|
||||
{ waitFor: 'scripted TUI ready.', occurrence: 2, send: '/exit\r' },
|
||||
],
|
||||
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, firstRunCopy.paragraphs[0]!, columns, 30))
|
||||
.toMatchFileSnapshot(join(firstRunSnapshots, `${String(columns)}-columns.expected.txt`))
|
||||
expect(output).toContain(TUI_FIRST_RUN_WELCOME_WHALE[tier].unicode[0]!.trim())
|
||||
expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`)
|
||||
expect(output).toContain('\u001B[?2004l')
|
||||
}, LOADER_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,
|
||||
columns: 60,
|
||||
rows: 12,
|
||||
actions: [
|
||||
{ waitFor: firstRunCopy.paragraphs[0]!, send: '\x1b[F' },
|
||||
{ waitFor: '企业微信群', send: '\r' },
|
||||
{ waitFor: 'scripted TUI ready.', occurrence: 2, send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
await expect(await firstRunFrameSnapshot(output, firstRunCopy.paragraphs[0]!, 60, 12))
|
||||
.toMatchFileSnapshot(join(firstRunSnapshots, '60-columns-low-height.expected.txt'))
|
||||
expect(output).toContain(firstRunCopy.title)
|
||||
expect(output).toContain(firstRunCopy.paragraphs[0])
|
||||
expect(output).toContain('企业微信群')
|
||||
expect(output).toContain(`Enter ${firstRunCopy.continueLabel}`)
|
||||
expect(output).not.toContain(TUI_FIRST_RUN_WELCOME_WHALE.minimal.unicode[0]!.trim())
|
||||
}, LOADER_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,
|
||||
actions: [
|
||||
{ waitFor: firstRunCopy.paragraphs[0]!, send: '\r' },
|
||||
{ waitFor: 'scripted TUI ready.', occurrence: 2, send: '/exit\r' },
|
||||
],
|
||||
})
|
||||
expect(first).toContain(firstRunCopy.title)
|
||||
|
||||
const second = await smoke({
|
||||
label: 'dsh second welcome launch',
|
||||
tempDirPrefix: 'unused-',
|
||||
cwd,
|
||||
configPath: scriptedConfigPath,
|
||||
showFirstRunWelcome: true,
|
||||
actions: [{ waitFor: 'main-session-', send: '/exit\r', delayMs: 1_500 }],
|
||||
})
|
||||
expect(second).not.toContain(firstRunCopy.paragraphs[0])
|
||||
expect(second).not.toContain(`Enter ${firstRunCopy.continueLabel}`)
|
||||
} finally {
|
||||
await rm(cwd, { recursive: true, force: true })
|
||||
}
|
||||
}, LOADER_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
|
||||
@@ -317,6 +454,51 @@ describe('dsh TUI keyless smoke (real Loader tree in a PTY)', () => {
|
||||
})
|
||||
|
||||
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,
|
||||
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: firstRunCopy.paragraphs[0]!, send: '\r' },
|
||||
{ waitFor: 'resume-target', occurrence: 2, send: '/exit\r' },
|
||||
],
|
||||
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.map(event => event.type)).toEqual([
|
||||
'session/end-seed',
|
||||
'command/run',
|
||||
'command/done',
|
||||
])
|
||||
expect(appended).not.toContainEqual(expect.objectContaining({ type: 'user/message' }))
|
||||
expect(appended).not.toContainEqual(expect.objectContaining({ type: 'turn/start' }))
|
||||
},
|
||||
})
|
||||
expect(output).toContain(firstRunCopy.paragraphs[0])
|
||||
expect(output).toContain('Resume selector design — DeepSeek Harness')
|
||||
}, LOADER_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',
|
||||
|
||||
Reference in New Issue
Block a user