|
|
@@ -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,7 +736,7 @@ 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 () => {
|
|
|
// The launcher resolves the checkout root three hops up from apps/cli/{src,lib};
|
|
|
@@ -514,5 +762,5 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
|
|
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)
|
|
|
})
|