2026-07-28 13:55:59 +08:00
import { createUserMessage , createMessage } from '@deepseek-ai/dsh-llm'
2026-07-25 00:10:37 +08:00
import { realpathSync } from 'node:fs'
import { mkdir , readdir , readFile , writeFile } from 'node:fs/promises'
2026-07-22 10:55:16 +08:00
import { dirname , join } from 'node:path'
2026-07-17 12:01:37 +08:00
import { fileURLToPath } from 'node:url'
import { describe , expect , it } from 'vitest'
2026-07-20 19:26:04 +08:00
import { LOADER_SMOKE_TEST_TIMEOUT_MS } from '@deepseek-ai/dsh-loader-smoke'
2026-07-26 23:44:52 +08:00
import { packChunkRuns , SessionId , type SessionEvent , type SessionHeader } from '@deepseek-ai/dsh-session'
2026-07-24 12:31:26 +08:00
import { logPath , toHeaderLine } from '../../../packages/session-persistence/session-persistence-jsonl/src/format.ts'
2026-07-22 15:19:35 +08:00
import { runTuiPtySmoke , type TuiPtySmokeOptions } from './pty-harness.ts'
2026-07-17 12:01:37 +08:00
2026-07-29 13:58:21 +08:00
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.
2026-07-18 22:31:04 +08:00
const scriptedConfigPath = fileURLToPath ( new URL ( './fixtures/tui-scripted.cordis.yml' , import . meta . url ) )
2026-07-17 12:01:37 +08:00
const tsconfigPath = fileURLToPath ( new URL ( '../../../tsconfig.json' , import . meta . url ) )
2026-07-22 15:19:35 +08:00
/**
2026-07-23 17:39:30 +08:00
* Seed the isolated process workspace: ordinary files land in `cwd`, personal
* files in the Harness home (`.dsh`), and skill bundles under the agents
* home's `skills/` root — the same trees `$DSH_HOME` /
* `$DSH_AGENTS_HOME` point the child at.
2026-07-22 15:19:35 +08:00
*/
function seedWorkspace (
2026-07-23 17:39:30 +08:00
files : {
workspace? : Record < string , string >
personal? : Record < string , string >
skills? : Record < string , string >
} ,
2026-07-22 15:19:35 +08:00
) : ( cwd : string ) = > Promise < void > {
return async ( cwd ) = > {
2026-07-23 17:39:30 +08:00
for ( const [ name , content ] of Object . entries ( files . workspace ? ? { } ) ) {
const file = join ( cwd , name )
await mkdir ( dirname ( file ) , { recursive : true } )
await writeFile ( file , content )
}
2026-07-22 15:19:35 +08:00
for ( const [ name , content ] of Object . entries ( files . personal ? ? { } ) ) {
const file = join ( cwd , '.dsh' , name )
await mkdir ( dirname ( file ) , { recursive : true } )
await writeFile ( file , content )
2026-07-22 10:55:16 +08:00
}
2026-07-22 15:19:35 +08:00
for ( const [ name , content ] of Object . entries ( files . skills ? ? { } ) ) {
const file = join ( cwd , '.agents' , 'skills' , name )
2026-07-22 10:55:16 +08:00
await mkdir ( dirname ( file ) , { recursive : true } )
await writeFile ( file , content )
}
}
}
2026-07-24 12:31:26 +08:00
/** Seed one real plaintext JSONL session for the `/resume` selector and host handoff smoke. */
async function seedResumeSession ( cwd : string ) : Promise < void > {
2026-07-25 00:10:37 +08:00
const sessionCwd = realpathSync . native ( cwd )
2026-07-24 12:31:26 +08:00
const id = SessionId ( 'resume-target' )
const meta : SessionHeader = { version : 0 , id , createdAt : 1_700_000_000_000 , cwd : sessionCwd }
const events : SessionEvent [ ] = [
{ type : 'turn/start' , seq : 0 , time : 1_700_000_000_001 , data : { turn : 1 , trigger : { kind : 'message' , source : { kind : 'user' } } } } ,
2026-07-28 13:55:59 +08:00
{ type : 'user/message' , seq : 1 , time : 1_700_000_000_002 , data : createUserMessage ( {
content : [ { type : 'text' , text : 'persisted prompt' } ] , source : { kind : 'user' } ,
} ) , surfaceOp : 'append' } ,
2026-07-24 12:31:26 +08:00
{ type : 'step/start' , seq : 2 , time : 1_700_000_000_003 , data : { turn : 1 , step : 1 } } ,
{ type : 'request/header' , seq : 3 , time : 1_700_000_000_004 , data : { header : { config : { provider : 'tui-scripted' , model : 'tui-scripted-model' } } , reason : 'initial' } } ,
2026-07-28 13:55:59 +08:00
{ type : 'assistant/message' , seq : 4 , time : 1_700_000_000_005 , data : {
turn : 1 , step : 1 ,
message : createMessage ( {
role : 'assistant' ,
content : [ { type : 'text' , text : 'persisted answer' } ] ,
source : {
kind : 'model' ,
. . . { provider : 'tui-scripted' , model : 'tui-scripted-model' } ,
} ,
} ) ,
} , surfaceOp : 'append' } ,
2026-07-24 12:31:26 +08:00
{ type : 'step/end' , seq : 5 , time : 1_700_000_000_006 , data : { turn : 1 , step : 1 } } ,
{ type : 'session/title' , seq : 6 , time : 1_700_000_000_007 , data : { title : 'Resume selector design' , messageSeqs : [ 1 ] , source : { kind : 'fallback' } } } ,
{ type : 'todo/write' , seq : 7 , time : 1_700_000_000_008 , data : { todos : [ { content : 'Preserve restored state' , status : 'in_progress' } ] } } ,
{ type : 'turn/end' , seq : 8 , time : 1_700_000_000_009 , data : { turn : 1 , reason : { kind : 'completed' } } } ,
]
const file = logPath ( join ( cwd , '.sessions' ) , sessionCwd , id , 'none' )
await mkdir ( dirname ( file ) , { recursive : true } )
await writeFile ( file , [
JSON . stringify ( toHeaderLine ( meta ) ) ,
2026-07-26 23:44:52 +08:00
. . . packChunkRuns ( events ) . map ( record = > JSON . stringify ( record ) ) ,
2026-07-24 12:31:26 +08:00
'' ,
] . join ( '\n' ) )
}
2026-07-27 23:50:38 +08:00
/** Model-visible startup context from the first request in the workspace's persisted session log. */
interface LoggedRequestContext {
2026-07-27 17:53:40 +08:00
/** The system prompt string the launcher sends. */
system : string
2026-07-27 23:50:38 +08:00
/** The durable skill-catalog message serialized to text. */
skillCatalog : string
2026-07-27 17:53:40 +08:00
}
2026-07-27 23:50:38 +08:00
async function readLoggedRequestContext ( cwd : string ) : Promise < LoggedRequestContext > {
2026-07-22 10:55:16 +08:00
const sessionsDir = join ( cwd , '.sessions' )
const entries = await readdir ( sessionsDir , { recursive : true } )
// A single keyless run writes one session log; the source section is global, so any log carries it.
const logRelPath = entries . find ( name = > name . endsWith ( '.jsonl' ) )
if ( logRelPath === undefined ) throw new Error ( ` no session log written under ${ sessionsDir } ` )
const lines = ( await readFile ( join ( sessionsDir , logRelPath ) , 'utf8' ) ) . split ( '\n' ) . filter ( Boolean )
2026-07-27 23:50:38 +08:00
let skillCatalog = ''
2026-07-22 10:55:16 +08:00
for ( const line of lines ) {
2026-07-27 23:50:38 +08:00
const event = JSON . parse ( line ) as SessionEvent
if (
event . type === 'user/message'
&& event . data . source . kind === 'plugin'
&& event . data . source . plugin === 'dsh-tool-skill'
) {
skillCatalog = JSON . stringify ( event . data . content )
2026-07-27 17:53:40 +08:00
}
if ( event . type === 'request/header' ) {
return {
2026-07-27 23:50:38 +08:00
system : event.data.header.system ? ? '' ,
skillCatalog ,
2026-07-27 17:53:40 +08:00
}
}
2026-07-22 10:55:16 +08:00
}
throw new Error ( ` session log ${ logRelPath } has no request/header event ` )
}
2026-07-29 13:58:21 +08:00
/**
* Shared defaults: the keyless key and the dsh bin. Each case supplies either
* `configArgs: []` (boot the shipped composition, `base.cordis.yml` +
* `tui.cordis.yml`, with no flags) or `configPath` (an overlay layered over that
* same base through `--config`).
*/
2026-07-22 15:19:35 +08:00
function smoke ( overrides : Partial < TuiPtySmokeOptions > & { label : string } ) : Promise < string > {
return runTuiPtySmoke ( {
2026-07-29 13:58:21 +08:00
tempDirPrefix : 'dsh-tui-smoke-' ,
2026-07-25 12:43:59 +08:00
binScript : dshBinScript ,
2026-07-22 15:19:35 +08:00
tsconfigPath ,
2026-07-31 01:33:33 +08:00
// 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' } ,
2026-07-29 22:27:30 +08:00
// Artifact CI builds and smokes concurrently on a contended runner.
. . . ( process . env . DSH_EXAMPLE_MODE === 'lib' ? { timeoutMs : 60_000 } : { } ) ,
2026-07-22 15:19:35 +08:00
. . . overrides ,
} )
}
// 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).
const SELECT_PRO_MODEL = [
{ waitFor : 'scripted TUI ready.' , send : '/model\r' } ,
2026-07-25 08:32:38 +08:00
{ waitFor : 'Select model' , send : '\x1b[B\x1b[Z\r' } ,
2026-07-22 15:19:35 +08:00
] as const
2026-07-29 13:58:21 +08:00
describe ( 'dsh TUI keyless smoke (real Loader tree in a PTY)' , ( ) = > {
2026-07-22 20:55:12 +08:00
it ( 'boots pi-tui, sweeps the borderless banner in, enters plan mode, and restores the terminal' , async ( ) = > {
2026-07-22 10:55:16 +08:00
// With no configured welcome the borderless banner sweeps in left-to-right;
2026-07-22 15:19:35 +08:00
// the detail line's session id (`main-session-<uuid>`) renders only once
// the sweep reaches it, so it marks a settled banner.
const output = await smoke ( {
2026-07-29 13:58:21 +08:00
label : 'dsh boot' ,
configArgs : [ ] ,
2026-07-22 14:32:35 +08:00
actions : [
2026-07-23 21:34:40 +08:00
{ waitFor : 'main-session-' , send : '/plan' } ,
{ waitFor : '[off|message] — Enter or leave plan mode' , send : '\r' } ,
2026-07-29 00:46:56 +08:00
{ waitFor : 'Plan mode on. Use /plan off to leave.' , send : '/exit\r' } ,
2026-07-22 14:32:35 +08:00
] ,
2026-07-20 19:26:04 +08:00
} )
2026-07-17 12:01:37 +08:00
expect ( output ) . toContain ( 'DEEPSEEK' )
2026-07-22 10:55:16 +08:00
expect ( output ) . toContain ( 'HARNESS' )
expect ( output ) . toContain ( 'main-session-' )
2026-07-23 21:34:40 +08:00
expect ( output ) . toContain ( '[off|message] — Enter or leave plan mode' )
2026-07-29 00:46:56 +08:00
expect ( output ) . toContain ( 'Plan mode on. Use /plan off to leave.' )
2026-07-22 10:55:16 +08:00
// Borderless: no box-drawing frame around the banner.
expect ( output ) . not . toContain ( '╭' )
expect ( output ) . not . toContain ( '╮' )
2026-07-18 22:31:04 +08:00
expect ( output ) . toContain ( '\u001B[?2004l' )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-20 22:44:01 +08:00
it ( 'switches models, streams a response, answers a user-question dialog, and exits cleanly' , async ( ) = > {
2026-07-22 15:19:35 +08:00
const output = await smoke ( {
2026-07-29 13:58:21 +08:00
label : 'dsh conversation' ,
tempDirPrefix : 'dsh-tui-conversation-' ,
2026-07-20 19:26:04 +08:00
configPath : scriptedConfigPath ,
actions : [
2026-07-22 15:19:35 +08:00
. . . SELECT_PRO_MODEL ,
2026-07-22 15:51:02 +08:00
{ waitFor : 'Model selected: tui-scripted/tui-scripted-model-pro.' , send : '/plan exercise the TUI\r' } ,
2026-07-21 14:50:06 +08:00
// The question text first appears in the streamed tool-call card. Wait
// for the dialog's input legend so Enter cannot arrive before it owns
// terminal input when pre-dispatch policy yields.
2026-07-21 20:09:37 +08:00
{ waitFor : 'Tab custom answer • ↑/↓ navigate • Enter submit • Esc interrupt' , send : '\r' } ,
2026-07-22 15:19:35 +08:00
{ waitFor : 'Decision received. Scripted TUI run complete.' , send : '' } ,
// Session title: the first user message drives the first-message-llm
// provider's tool-less title call; the scripted adapter answers it, the
// accepted title lands in the log, and the TUI renders the terminal
// window title as `<session title> — <configured title>` via OSC 0.
2026-07-22 03:10:14 -07:00
// Gating /status on it keeps the assertion race-free; the diagnostics
// card is then exercised through the same real Loader/PTY composition.
2026-07-23 21:06:03 +08:00
{ waitFor : 'scripted session title — DeepSeek Harness' , send : '/plan off\r' } ,
2026-07-29 00:46:56 +08:00
{ waitFor : 'Plan mode off.' , send : 'Confirm the scripted run left plan mode.\r' } ,
2026-07-23 21:06:03 +08:00
{ waitFor : 'Default mode confirmed.' , send : '/status\r' } ,
2026-07-22 03:29:59 -07:00
{ waitFor : 'Session status' , send : '/exit\r' } ,
2026-07-20 19:26:04 +08:00
] ,
} )
2026-07-18 22:31:04 +08:00
expect ( output ) . toContain ( 'I need one decision before I continue.' )
2026-07-25 08:32:38 +08:00
expect ( output ) . toContain ( 'Reasoning effort: Max.' )
2026-07-29 00:46:56 +08:00
expect ( output ) . toContain ( 'Plan mode on. Use /plan off to leave.' )
expect ( output ) . toContain ( 'Plan mode off.' )
2026-07-23 21:06:03 +08:00
expect ( output ) . toContain ( 'Default mode confirmed.' )
2026-07-19 10:49:59 +08:00
expect ( output ) . toContain ( String . raw ` \ x1b]2;MODEL_CONTROLLED \ x07 ` )
expect ( output ) . toContain ( String . raw ` \ x1b[999CMODEL_CURSOR ` )
expect ( output ) . toContain ( String . raw ` \ x9b31mMODEL_C1 ` )
expect ( output ) . not . toContain ( '\u001B]2;MODEL_CONTROLLED\u0007' )
expect ( output ) . not . toContain ( '\u001B[999CMODEL_CURSOR' )
expect ( output ) . not . toContain ( '\u009B31mMODEL_C1' )
2026-07-18 22:31:04 +08:00
expect ( output ) . toContain ( 'Safe' )
2026-07-22 15:01:54 +08:00
expect ( output ) . toContain ( '\u001B]0;scripted session title — DeepSeek Harness\u0007' )
2026-07-22 03:29:59 -07:00
expect ( output ) . toContain ( 'Session status' )
2026-07-22 03:10:14 -07:00
expect ( output ) . toContain ( 'Title' )
expect ( output ) . toContain ( 'scripted session title' )
expect ( output ) . toContain ( 'Model' )
expect ( output ) . toContain ( 'tui-scripted/tui-scripted-model-pro' )
2026-07-22 03:29:59 -07:00
expect ( output ) . toContain ( 'KV cache' )
2026-07-22 03:10:14 -07:00
expect ( output ) . toContain ( 'Context' )
expect ( output ) . toContain ( '128,000' )
2026-07-27 17:53:40 +08:00
expect ( output ) . toContain ( 'System prompt' )
expect ( output ) . toContain ( 'You are an AI agent powered by the DeepSeek Harness SDK.' )
expect ( output ) . toContain ( 'Registered tools' )
expect ( output ) . toContain ( 'ask_user_question' )
2026-07-22 10:55:16 +08:00
expect ( output ) . toContain ( '\u001B[?2004l' )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
it ( 'loads a local skill via /skill: and delivers its body to the model as a user turn' , async ( ) = > {
2026-07-29 23:17:07 +08:00
// The whole user-only invocation path in one keyless boot: `ctx.get('skills')`
2026-07-22 10:55:16 +08:00
// resolves in the shipped tree, the client-side `/skill:` command parses,
2026-07-29 23:17:07 +08:00
// and the local provider admits a model-disabled skill by the omitted
// `user-invocable` default. The rendered `<skill name="…">` block reaches
// the model — proven by the scripted adapter echoing the fixture's body
// marker only when it arrives.
2026-07-22 15:19:35 +08:00
const output = await smoke ( {
2026-07-29 13:58:21 +08:00
label : 'dsh skill' ,
tempDirPrefix : 'dsh-tui-skill-' ,
2026-07-22 15:19:35 +08:00
configPath : scriptedConfigPath ,
prepare : seedWorkspace ( {
skills : {
'scripted-skill/SKILL.md' : [
'---' ,
'name: scripted-skill' ,
'description: Keyless PTY proof that the skill command loads a local skill into the conversation.' ,
2026-07-28 17:22:41 +08:00
'disable-model-invocation: true' ,
2026-07-22 15:19:35 +08:00
'---' ,
'' ,
'SCRIPTED SKILL BODY MARKER' ,
'' ,
] . join ( '\n' ) ,
} ,
} ) ,
actions : [
. . . SELECT_PRO_MODEL ,
{ waitFor : 'Model selected: tui-scripted/tui-scripted-model-pro.' , send : '/skill:scripted-skill\r' } ,
{ waitFor : 'Scripted skill body received.' , send : '/exit\r' } ,
] ,
2026-07-22 10:55:16 +08:00
} )
2026-07-27 17:53:40 +08:00
expect ( output ) . not . toContain ( '[instructions]' )
2026-07-22 10:55:16 +08:00
expect ( output ) . toContain ( 'Scripted skill body received.' )
expect ( output ) . toContain ( '\u001B[?2004l' )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-27 16:50:56 +08:00
it ( 'adds a watched local skill to live /skill: autocomplete without restarting' , async ( ) = > {
const skill = [
'---' ,
'name: hot-added-skill' ,
'description: HOT_ADDED_COMPLETION_MARKER' ,
'---' ,
'' ,
'Hot-added body.' ,
'' ,
] . join ( '\n' )
const output = await smoke ( {
label : 'tui-agent hot-added skill autocomplete' ,
tempDirPrefix : 'tui-agent-hot-skill-' ,
configPath : scriptedConfigPath ,
actions : [
{
waitFor : 'scripted TUI ready.' ,
writeFile : {
path : '.agents/skills/hot-added-skill/SKILL.md' ,
content : skill ,
} ,
send : '/skill:hot' ,
} ,
{ waitFor : 'HOT_ADDED_COMPLETION_MARKER' , send : '\x03/exit\r' } ,
] ,
} )
expect ( output ) . toContain ( 'HOT_ADDED_COMPLETION_MARKER' )
expect ( output ) . toContain ( '\u001B[?2004l' )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-30 00:09:03 +08:00
it . skipIf ( process . env . DSH_EXAMPLE_MODE === 'lib' ) ( 'fuzzy-completes an @file path without reading or submitting the file' , async ( ) = > {
2026-07-23 17:39:30 +08:00
const output = await smoke ( {
2026-07-29 13:58:21 +08:00
label : 'dsh file autocomplete' ,
tempDirPrefix : 'dsh-tui-file-autocomplete-' ,
2026-07-30 00:09:03 +08:00
// Source-plane PTY coverage complements the deterministic package-level
// autocomplete tests. Artifact CI omits this timing-sensitive terminal
// rendering assertion; built boot is covered by the neighboring cases.
2026-07-29 13:58:21 +08:00
configArgs : [ ] ,
2026-07-23 17:39:30 +08:00
prepare : seedWorkspace ( {
workspace : {
'src/terminal-special-case.ts' : 'export const marker = true\n' ,
'src/other.ts' : 'export const other = true\n' ,
} ,
} ) ,
actions : [
{ waitFor : 'main-session-' , send : '@tsc' } ,
{ waitFor : 'File · terminal-special-case.t' , send : '\t' } ,
{ waitFor : '@src/terminal-special-case.ts' , send : '\x03/exit\r' } ,
] ,
} )
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 )
2026-07-17 12:01:37 +08:00
} )
2026-07-22 10:55:16 +08:00
describe ( 'dsh CLI keyless smoke (apps/cli through the same PTY)' , ( ) = > {
2026-07-24 16:07:49 +08:00
it ( 'exec-replaces the TUI for /resume and restores the same session state' , async ( ) = > {
2026-07-24 12:31:26 +08:00
const output = await smoke ( {
label : 'dsh in-place resume' ,
tempDirPrefix : 'dsh-in-place-resume-' ,
binScript : dshBinScript ,
2026-07-25 15:47:55 +08:00
configPath : scriptedConfigPath ,
2026-07-24 12:31:26 +08:00
prepare : seedResumeSession ,
actions : [
{ waitFor : 'scripted TUI ready.' , send : '/resume\r' } ,
{ waitFor : 'Resume selector design' , send : 'Resume selector design' } ,
2026-07-23 22:40:17 -07:00
{ waitFor : '⌕ Resume selector design' , send : '\r' } ,
2026-07-24 12:31:26 +08:00
{ waitFor : 'Preserve restored state' , send : '/exit\r' } ,
] ,
} )
const released = output . indexOf ( '\u001B[?2004l' )
const restored = output . indexOf ( 'Resume selector design — DeepSeek Harness' )
expect ( released ) . toBeGreaterThanOrEqual ( 0 )
expect ( restored ) . toBeGreaterThan ( released )
expect ( output ) . toContain ( 'Preserve restored state' )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-22 10:55:16 +08:00
it ( 'boots the shipped default config with no arguments and no personal overlay' , async ( ) = > {
2026-07-22 15:19:35 +08:00
const output = await smoke ( {
label : 'dsh default boot' ,
tempDirPrefix : 'dsh-default-boot-' ,
binScript : dshBinScript ,
configArgs : [ ] ,
actions : [ { waitFor : 'main-session-' , send : '/exit\r' } ] ,
} )
2026-07-22 10:55:16 +08:00
expect ( output ) . toContain ( 'DEEPSEEK' )
expect ( output ) . toContain ( 'main-session-' )
expect ( output ) . not . toContain ( '╭' )
expect ( output ) . not . toContain ( '╮' )
expect ( output ) . toContain ( '\u001B[?2004l' )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-30 19:46:04 +08:00
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 ( ) = > {
2026-07-30 17:09:29 +08:00
// The whole personal-config chain in one boot, plus the environment layer
2026-07-30 19:46:04 +08:00
// it deliberately excludes. config.yaml patches the `tui` row — a row the
// SURFACE OVERLAY inserted, not one the base declares — proving a later
// patch list reaches a row an earlier one inserted. The single `!!js`
// expression prefers the PERSONAL variable, so the welcome can only render
// the project value while the harness home's .env — the credential store
// of `dsh-credentials-local` — is NOT hoisted into `process.env`; hoisting
// it would make every stored key read as a read-only launch override on
// the next run and hand it to every subprocess the agent starts.
2026-07-22 15:19:35 +08:00
const output = await smoke ( {
label : 'dsh personal overlay' ,
tempDirPrefix : 'dsh-personal-overlay-' ,
binScript : dshBinScript ,
2026-07-22 10:55:16 +08:00
configArgs : [ ] ,
2026-07-22 15:19:35 +08:00
prepare : seedWorkspace ( {
2026-07-30 17:09:29 +08:00
workspace : { '.env' : 'DSH_PROJECT_WELCOME=PROJECT OVERLAY READY.\n' } ,
2026-07-22 15:19:35 +08:00
personal : {
2026-07-30 17:09:29 +08:00
'.env' : 'DSH_PERSONAL_WELCOME=HOME ENV LEAKED.\n' ,
2026-07-22 15:19:35 +08:00
'config.yaml' : [
2026-07-29 13:58:21 +08:00
'- id: workspace-context' ,
' disabled: true' ,
'- id: tui' ,
2026-07-22 15:19:35 +08:00
' config:' ,
2026-07-29 13:58:21 +08:00
" sessionId: !!js configuredAgentIdentities?.main?.id ?? 'main'" ,
2026-07-30 17:09:29 +08:00
' welcome: !!js process.env.DSH_PERSONAL_WELCOME ?? process.env.DSH_PROJECT_WELCOME' ,
2026-07-22 15:19:35 +08:00
'' ,
] . join ( '\n' ) ,
} ,
} ) ,
2026-07-30 17:09:29 +08:00
actions : [ { waitFor : 'PROJECT OVERLAY READY.' , send : '/exit\r' } ] ,
2026-07-22 10:55:16 +08:00
} )
2026-07-30 17:09:29 +08:00
expect ( output ) . toContain ( 'PROJECT OVERLAY READY.' )
expect ( output ) . not . toContain ( 'HOME ENV LEAKED.' )
2026-07-22 10:55:16 +08:00
expect ( output ) . toContain ( '\u001B[?2004l' )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
it ( 'fails loud instead of booting when the personal config.yaml is invalid' , async ( ) = > {
2026-07-22 15:19:35 +08:00
const output = await smoke ( {
label : 'dsh invalid personal config' ,
tempDirPrefix : 'dsh-invalid-personal-' ,
binScript : dshBinScript ,
2026-07-22 10:55:16 +08:00
configArgs : [ ] ,
2026-07-22 15:19:35 +08:00
prepare : seedWorkspace ( { personal : { 'config.yaml' : 'id: not-a-list\n' } } ) ,
expectedExitCode : 1 ,
} )
expect ( output ) . toContain ( 'must be a top-level YAML array of loader patch entries' )
2026-07-22 10:55:16 +08:00
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-29 14:29:32 +08:00
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`,
// provides it as the launcher-owned identity on the boot context, and the
// resume fails loud — proving the printed hint reaches the app's resume
// intake with no config key and no environment variable.
2026-07-22 15:19:35 +08:00
const output = await smoke ( {
label : 'dsh resume flag failure' ,
tempDirPrefix : 'dsh-resume-flag-' ,
binScript : dshBinScript ,
2026-07-22 10:55:16 +08:00
configArgs : [ '--resume' , 'missing-session' ] ,
2026-07-22 15:19:35 +08:00
expectedExitCode : 1 ,
2026-07-20 19:26:04 +08:00
} )
2026-07-19 11:37:14 +08:00
expect ( output ) . toContain ( 'ui-tui: session "missing-session" failed to start:' )
2026-07-17 16:44:25 +08:00
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-22 10:55:16 +08:00
2026-07-29 14:29:32 +08:00
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
// `--config`: a hint that omitted it would resume into the default tree.
const output = await smoke ( {
label : 'dsh goodbye message' ,
tempDirPrefix : 'dsh-goodbye-' ,
binScript : dshBinScript ,
configPath : scriptedConfigPath ,
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 )
2026-07-29 13:58:21 +08:00
it ( 'keeps resume working when the personal overlay replaces the whole agent-loop config' , async ( ) = > {
2026-07-29 14:29:32 +08:00
// Loader patches replace a targeted `config` key wholesale, so a personal
2026-07-29 13:58:21 +08:00
// overlay repointing the model route drops every identity key the shipped
// row declared. Launcher-owned identity makes that unreachable: agent-loop
// applies the launcher's id over whatever route survives.
2026-07-29 14:29:32 +08:00
const output = await smoke ( {
label : 'dsh overlay keeps resume' ,
tempDirPrefix : 'dsh-overlay-resume-' ,
binScript : dshBinScript ,
configArgs : [ ] ,
prepare : seedWorkspace ( {
personal : {
'config.yaml' : [
2026-07-29 13:58:21 +08:00
'- id: workspace-context' ,
' disabled: true' ,
'- id: agent-loop' ,
' config:' ,
' agents:' ,
' - id: main' ,
2026-07-30 20:15:40 +08:00
' provider: deepseek-official' ,
2026-07-29 13:58:21 +08:00
' model: deepseek-v4-flash' ,
' cwd: !!js process.cwd()' ,
'- id: tui' ,
2026-07-29 14:29:32 +08:00
' config:' ,
2026-07-29 13:58:21 +08:00
" sessionId: !!js configuredAgentIdentities?.main?.id ?? 'main'" ,
2026-07-29 14:29:32 +08:00
' welcome: OVERLAY REPLACED THE CONFIG.' ,
'' ,
] . join ( '\n' ) ,
} ,
} ) ,
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 )
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
// consumes into its own `[exit 3]` pill. Rendering both would report the same
// exit twice, so the marker must not survive into the card body.
const output = await smoke ( {
2026-07-29 13:58:21 +08:00
label : 'dsh bash exit pill' ,
2026-07-29 14:29:32 +08:00
tempDirPrefix : 'dsh-bash-exit-pill-' ,
configPath : scriptedConfigPath ,
actions : [
. . . SELECT_PRO_MODEL ,
{
waitFor : 'Model selected: tui-scripted/tui-scripted-model-pro.' ,
send : 'Run the failing scripted command.\r' ,
} ,
{ waitFor : 'Scripted bash failure observed.' , send : '/exit\r' } ,
] ,
} )
// The command really ran: its stdout is in the card body.
expect ( output ) . toContain ( 'SCRIPTED_BASH_FAILED' )
expect ( output ) . toContain ( '[exit 3]' )
expect ( output ) . not . toContain ( '[exit code: 3]' )
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-27 17:53:40 +08:00
it ( 'tells the model its source path and offers the bundled maintenance skills' , async ( ) = > {
2026-07-22 10:55:16 +08:00
// 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.
2026-07-27 17:53:40 +08:00
// The source-path line is a system-prompt section; the bundled skills reach the
2026-07-27 23:50:38 +08:00
// model through a durable user message, so each assertion targets its own field.
2026-07-22 10:55:16 +08:00
const sourceRoot = fileURLToPath ( new URL ( '../../..' , import . meta . url ) )
2026-07-27 23:50:38 +08:00
let context : LoggedRequestContext = { system : '' , skillCatalog : '' }
2026-07-22 15:19:35 +08:00
await smoke ( {
label : 'dsh source-path prompt' ,
tempDirPrefix : 'dsh-source-path-' ,
binScript : dshBinScript ,
2026-07-25 15:47:55 +08:00
configPath : scriptedConfigPath ,
2026-07-22 15:19:35 +08:00
actions : [
. . . SELECT_PRO_MODEL ,
{ waitFor : 'Model selected: tui-scripted/tui-scripted-model-pro.' , send : 'exercise the TUI\r' } ,
{ waitFor : 'How should the scripted run proceed?' , send : '\r' } ,
{ waitFor : 'Decision received. Scripted TUI run complete.' , send : '/exit\r' } ,
] ,
2026-07-27 23:50:38 +08:00
inspect : async ( cwd ) = > { context = await readLoggedRequestContext ( cwd ) } ,
2026-07-22 10:55:16 +08:00
} )
2026-07-27 23:50:38 +08:00
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 . 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.' )
2026-07-22 10:55:16 +08:00
} , LOADER_SMOKE_TEST_TIMEOUT_MS )
2026-07-17 12:01:37 +08:00
} )