Merge origin/master: web permission sandbox, default pi-ai providers
This commit is contained in:
@@ -60,7 +60,6 @@
|
||||
config:
|
||||
provider: deepseek
|
||||
model: deepseek-v4-pro
|
||||
resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"
|
||||
persistenceRoot: './.sessions'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
|
||||
38
examples/headless-agent/tests/fixtures/guard/source-guard/cordis.yml
vendored
Normal file
38
examples/headless-agent/tests/fixtures/guard/source-guard/cordis.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
# Test-only composition: the model attempts one `write` into a staging-shaped
|
||||
# git fixture, so the guard's denial is observed through the real Loader and app.
|
||||
- id: source-guard-mock-llm
|
||||
name: './mock-llm.ts'
|
||||
|
||||
# Managed child-process groups for the bash executor (spawn/kill/output plumbing).
|
||||
- id: subprocess
|
||||
name: '@deepseek-ai/dsh-subprocess-local'
|
||||
|
||||
- id: bash
|
||||
name: '@deepseek-ai/dsh-bash-local'
|
||||
|
||||
- id: fs
|
||||
name: '@deepseek-ai/dsh-fs-local'
|
||||
|
||||
# Read-before-edit policy: without it the write would resolve `createIfAbsent`
|
||||
# and the transcript would not show the guard as the sole reason for refusal.
|
||||
- id: fs-policy
|
||||
name: '@deepseek-ai/dsh-fs-policy'
|
||||
|
||||
- id: tool-fs
|
||||
name: '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
# Mounts the guard with `protectedCheckout` resolved against the process cwd, so
|
||||
# it arms for the staging fixture the smoke builds there rather than for the
|
||||
# checkout running the test (the config default is this module's own location).
|
||||
- id: source-guard-fixture
|
||||
name: './mount-guard.ts'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
config:
|
||||
provider: source-guard-mock
|
||||
model: source-guard-mock
|
||||
persona: 'Test the source guard.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: none
|
||||
workspaceContext: false
|
||||
43
examples/headless-agent/tests/fixtures/guard/source-guard/mock-llm.ts
vendored
Normal file
43
examples/headless-agent/tests/fixtures/guard/source-guard/mock-llm.ts
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
import { resolve } from 'node:path'
|
||||
import type { Context } from 'cordis'
|
||||
import { CallId, LlmAdapter, type GenerateOptions, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
/** The staged file the smoke builds in the process cwd; the guard must refuse to write it. */
|
||||
const TARGET = resolve('staging/guarded.ts')
|
||||
|
||||
/**
|
||||
* Two-step adapter for the source-guard Loader fixture: the first step calls
|
||||
* `write` on the staged file, the second closes the turn once a tool result has
|
||||
* come back, so the transcript records what the model received.
|
||||
*/
|
||||
class SourceGuardMockAdapter extends LlmAdapter {
|
||||
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
||||
const alreadyCalled = options.messages.some(message => message.content.some(
|
||||
block => block.type === 'tool-result',
|
||||
))
|
||||
if (alreadyCalled) {
|
||||
const text = 'denied as expected'
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
yield { type: 'text-delta', index: 0, text }
|
||||
yield { type: 'block-end', index: 0, block: { type: 'text', text } }
|
||||
yield { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
return
|
||||
}
|
||||
const callId = CallId('source-guard-write')
|
||||
const args = JSON.stringify({ file_path: TARGET, content: 'edited\n' })
|
||||
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
|
||||
yield { type: 'tool-call-delta', index: 0, id: callId, name: 'write', argumentsDelta: args }
|
||||
yield { type: 'block-end', index: 0, block: { type: 'tool-call', id: callId, name: 'write', arguments: args } }
|
||||
yield { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }
|
||||
yield { type: 'finish', reason: { kind: 'tool-calls' } }
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'source-guard-mock-llm'
|
||||
export const inject = ['llm']
|
||||
|
||||
/** Register the test-only `source-guard-mock` adapter. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.llm.registerAdapter(['source-guard-mock'], new SourceGuardMockAdapter())
|
||||
}
|
||||
14
examples/headless-agent/tests/fixtures/guard/source-guard/mount-guard.ts
vendored
Normal file
14
examples/headless-agent/tests/fixtures/guard/source-guard/mount-guard.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
import { resolve } from 'node:path'
|
||||
import type { Context } from 'cordis'
|
||||
import * as SourceGuard from '@deepseek-ai/dsh-source-guard'
|
||||
|
||||
export const name = 'source-guard-fixture'
|
||||
|
||||
/**
|
||||
* Mount the real guard against the staging fixture in the process cwd. The
|
||||
* checkout under protection is a runtime fact of the isolated smoke directory,
|
||||
* which no static config value can name.
|
||||
*/
|
||||
export async function apply(ctx: Context): Promise<void> {
|
||||
await ctx.plugin(SourceGuard, { protectedCheckout: resolve('staging/guard-anchor.ts') })
|
||||
}
|
||||
16
examples/headless-agent/tests/fixtures/tmux-context-driver.ts
vendored
Normal file
16
examples/headless-agent/tests/fixtures/tmux-context-driver.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
/** Test driver that sends two turns through one Headless Loader composition. */
|
||||
|
||||
import { boot, resolveConfigPath } from '@deepseek-ai/dsh-app-boot'
|
||||
import { runOneShot } from '@deepseek-ai/dsh-cli-demo/src/cli.ts'
|
||||
|
||||
const configPath = process.argv[2]
|
||||
if (configPath === undefined) throw new Error('tmux-context driver requires a config path')
|
||||
|
||||
const ctx = await boot('tmux-context-e2e', resolveConfigPath(configPath, undefined))
|
||||
try {
|
||||
await runOneShot(ctx, { task: 'first' })
|
||||
await runOneShot(ctx, { task: 'second' })
|
||||
} finally {
|
||||
await ctx.fiber.dispose()
|
||||
}
|
||||
46
examples/headless-agent/tests/fixtures/tmux-context-mock-bash.ts
vendored
Normal file
46
examples/headless-agent/tests/fixtures/tmux-context-mock-bash.ts
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { Context } from 'cordis'
|
||||
import { BashExecutor } from '@deepseek-ai/dsh-bash'
|
||||
import type { BashExecRequest, BashExecSpec, BashProcess, BashRunResult } from '@deepseek-ai/dsh-bash'
|
||||
|
||||
/**
|
||||
* Deterministic `ctx.bash` for the tmux-context Loader fixture: any command
|
||||
* (the plugin's `tmux display-message`) returns a fixed tab-delimited reading,
|
||||
* so the injected tmux location is stable without a real tmux server. `start()`
|
||||
* throws — tmux-context must never spawn a background process.
|
||||
*/
|
||||
class TmuxMockBash extends BashExecutor {
|
||||
override resolve(request: BashExecRequest): BashExecSpec {
|
||||
return {
|
||||
command: request.command,
|
||||
workdir: request.workdir ?? process.cwd(),
|
||||
timeoutMs: request.timeoutMs ?? 60_000,
|
||||
stdoutMaxBytes: request.stdoutMaxBytes ?? 64_000,
|
||||
signal: request.signal,
|
||||
sandboxPolicy: request.sandboxPolicy,
|
||||
}
|
||||
}
|
||||
|
||||
override run(_spec: BashExecSpec): Promise<BashRunResult> {
|
||||
const line = ['work', '0', 'editor', '1', '%3', '1', '1', 'a1b2,80x24,0,0,4'].join('\\t')
|
||||
return Promise.resolve({
|
||||
exitCode: 0,
|
||||
signal: null,
|
||||
timedOut: false,
|
||||
aborted: false,
|
||||
timeoutMs: 60_000,
|
||||
stdout: { text: `${line}\n`, truncated: false },
|
||||
stderr: { text: '', truncated: false },
|
||||
})
|
||||
}
|
||||
|
||||
override start(): BashProcess {
|
||||
throw new Error('tmux-context must never start a background task')
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'tmux-context-mock-bash'
|
||||
|
||||
/** Register the deterministic `ctx.bash` executor for the fixture. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.plugin(TmuxMockBash)
|
||||
}
|
||||
22
examples/headless-agent/tests/fixtures/tmux-context-mock-llm.ts
vendored
Normal file
22
examples/headless-agent/tests/fixtures/tmux-context-mock-llm.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Context } from 'cordis'
|
||||
import { LlmAdapter, type StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
|
||||
/** Deterministic one-step adapter for the tmux-context Loader fixture. */
|
||||
class TmuxContextMockAdapter extends LlmAdapter {
|
||||
async * stream(): AsyncIterable<StreamChunk> {
|
||||
const text = 'tmux context sampled'
|
||||
yield { type: 'block-start', index: 0, blockType: 'text' }
|
||||
yield { type: 'text-delta', index: 0, text }
|
||||
yield { type: 'block-end', index: 0, block: { type: 'text', text } }
|
||||
yield { type: 'usage', usage: { inputTokens: 1, outputTokens: 1 } }
|
||||
yield { type: 'finish', reason: { kind: 'stop' } }
|
||||
}
|
||||
}
|
||||
|
||||
export const name = 'tmux-context-mock-llm'
|
||||
export const inject = ['llm']
|
||||
|
||||
/** Register the test-only `tmux-context-mock` adapter. */
|
||||
export function apply(ctx: Context): void {
|
||||
ctx.llm.registerAdapter(['tmux-context-mock'], new TmuxContextMockAdapter())
|
||||
}
|
||||
21
examples/headless-agent/tests/fixtures/tmux-context.cordis.yml
vendored
Normal file
21
examples/headless-agent/tests/fixtures/tmux-context.cordis.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Test-only composition: keep tmux-context opt-in while exercising its real Loader/app path.
|
||||
# A deterministic mock ctx.bash returns a fixed tmux reading, so the injected location
|
||||
# is stable without a real tmux server on the test host.
|
||||
- id: tmux-context-mock-llm
|
||||
name: './tmux-context-mock-llm.ts'
|
||||
|
||||
- id: bash
|
||||
name: './tmux-context-mock-bash.ts'
|
||||
|
||||
- id: tmux-context
|
||||
name: '@deepseek-ai/dsh-tmux-context'
|
||||
|
||||
- id: cli-agent
|
||||
name: '@deepseek-ai/dsh-cli-demo'
|
||||
config:
|
||||
provider: tmux-context-mock
|
||||
model: tmux-context-mock
|
||||
persona: 'Test the tmux-context plugin.'
|
||||
persistenceRoot: './.sessions'
|
||||
persistenceCompression: 'none'
|
||||
workspaceContext: false
|
||||
@@ -10,6 +10,7 @@
|
||||
"@deepseek-ai/dsh-acp-demo": "workspace:*",
|
||||
"@deepseek-ai/dsh-agent-spine-demo": "workspace:*",
|
||||
"@deepseek-ai/dsh-app-boot": "workspace:*",
|
||||
"@deepseek-ai/dsh-bash": "workspace:*",
|
||||
"@deepseek-ai/dsh-bash-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-bash-sandbox": "workspace:*",
|
||||
"@deepseek-ai/dsh-cli-demo": "workspace:*",
|
||||
@@ -44,6 +45,7 @@
|
||||
"@deepseek-ai/dsh-session-query-sqlite": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-telemetry-otel": "workspace:*",
|
||||
"@deepseek-ai/dsh-session-title-first-message-llm": "workspace:*",
|
||||
"@deepseek-ai/dsh-source-guard": "workspace:*",
|
||||
"@deepseek-ai/dsh-spill-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-spill-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-subagent": "workspace:*",
|
||||
@@ -54,6 +56,7 @@
|
||||
"@deepseek-ai/dsh-tasks-local": "workspace:*",
|
||||
"@deepseek-ai/dsh-time-context": "workspace:*",
|
||||
"@deepseek-ai/dsh-timeout-policy": "workspace:*",
|
||||
"@deepseek-ai/dsh-tmux-context": "workspace:*",
|
||||
"@deepseek-ai/dsh-token-meter": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-ask-user": "workspace:*",
|
||||
"@deepseek-ai/dsh-tool-cordis": "workspace:*",
|
||||
|
||||
@@ -29,7 +29,7 @@ Each run starts a fresh session by default (its event log lands under `./.sessio
|
||||
dsh --resume <prior-session-id>
|
||||
```
|
||||
|
||||
`/resume` opens a searchable keyboard selector with titles, activity, last-turn results, model route, durable goal phase, and live/persisted state. The installed `dsh` host flushes and disposes the current app, then replaces the process with `dsh --resume <id>`. The TUI still prints that command on exit and shows it when a custom host cannot hand off. `dsh --resume <id>` provides the id on the boot context, which `cordis.yml` reads (`resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"`); with no flag the agent starts a new session. A missing or unreadable id starts no agent and emits `agent-loop/config-start-failed`: the TUI prints the failure and exits nonzero. The selector has no cross-process session lock, so deployments with concurrent hosts must coordinate session ownership separately.
|
||||
`/resume` opens a searchable keyboard selector with titles, activity, last-turn results, model route, durable goal phase, and live/persisted state. The installed `dsh` host flushes and disposes the current app, then replaces the process with `dsh --resume <id>`; a host that cannot hand off in place says so and leaves the session running. Resume needs no key in this file: `dsh` provides the session identity and the exit line on the boot context, so `--resume <id>` and the printed resume command survive any personal-overlay patch of the `tui-agent` entry. With no flag the agent starts a new session. A missing or unreadable id starts no agent and emits `agent-loop/config-start-failed`: the TUI prints the failure and exits nonzero. The selector has no cross-process session lock, so deployments with concurrent hosts must coordinate session ownership separately.
|
||||
|
||||
## Code Mode
|
||||
|
||||
|
||||
@@ -10,9 +10,7 @@
|
||||
config:
|
||||
provider: deepseek
|
||||
model: deepseek-v4-pro
|
||||
resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"
|
||||
persistenceRoot: './.sessions'
|
||||
resumeCommand: 'dsh --resume {session}'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
tools:
|
||||
|
||||
@@ -59,6 +59,8 @@ flowchart LR
|
||||
cfg --> plugin_tui_fs_policy
|
||||
plugin_tui_tool_fs["tool-fs<br/>@deepseek-ai/dsh-tool-fs"]
|
||||
cfg --> plugin_tui_tool_fs
|
||||
plugin_tui_source_guard["source-guard<br/>@deepseek-ai/dsh-source-guard"]
|
||||
cfg --> plugin_tui_source_guard
|
||||
plugin_tui_tool_fs_search["tool-fs-search<br/>@deepseek-ai/dsh-tool-fs-search"]
|
||||
cfg --> plugin_tui_tool_fs_search
|
||||
plugin_tui_timeout_policy["timeout-policy<br/>@deepseek-ai/dsh-timeout-policy"]
|
||||
@@ -93,6 +95,7 @@ flowchart LR
|
||||
| `fs-local` | `@deepseek-ai/dsh-fs-local` |
|
||||
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
|
||||
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
|
||||
| `source-guard` | `@deepseek-ai/dsh-source-guard` |
|
||||
| `tool-fs-search` | `@deepseek-ai/dsh-tool-fs-search` |
|
||||
| `timeout-policy` | `@deepseek-ai/dsh-timeout-policy` |
|
||||
| `spill-local` | `@deepseek-ai/dsh-spill-local` |
|
||||
|
||||
@@ -47,15 +47,11 @@
|
||||
config:
|
||||
provider: deepseek
|
||||
model: deepseek-v4-pro
|
||||
# `dsh --resume <id>` provides the session id on the boot context (the ids
|
||||
# live under ./.sessions); with no flag the identifier is undefined and a
|
||||
# fresh session starts each run. The typeof guard tolerates a launcher that
|
||||
# never provides the slot, reading undefined rather than throwing.
|
||||
resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"
|
||||
persistenceRoot: './.sessions'
|
||||
# Printed on exit and listed by `/resume`; `{session}` fills the live id.
|
||||
# `dsh --resume <id>` resumes that session, so run it from this cwd.
|
||||
resumeCommand: 'dsh --resume {session}'
|
||||
# Session identity and the resume command printed on exit are launcher-owned:
|
||||
# `dsh` provides both on the boot context, so `--resume <id>` and the exit
|
||||
# hint need no key here. `persistenceRoot` is omitted: the dsh launcher
|
||||
# supplies its shared Harness-home store through a boot slot, and a bare
|
||||
# example boot falls back to the bundle's project-local `./.sessions`.
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
ui:
|
||||
@@ -169,6 +165,15 @@
|
||||
- id: tool-fs
|
||||
name: '@deepseek-ai/dsh-tool-fs'
|
||||
|
||||
# Refuses write/edit inside the dsh checkout this launcher runs from, on that
|
||||
# checkout's own branch, until the session loads dsh-customize — the skill whose
|
||||
# workflow (task worktree, then integrate under the staging lock) the refusal
|
||||
# points at. Inert everywhere else: another repository, a task worktree nested
|
||||
# under the protected one, a sibling checkout on a different branch, and any
|
||||
# workspace outside a dsh source install all pass through untouched.
|
||||
- id: source-guard
|
||||
name: '@deepseek-ai/dsh-source-guard'
|
||||
|
||||
# Bash-backed discovery tools (glob/grep): fixed ripgrep commands through the
|
||||
# local bash executor above — not ctx.fs. Capped results save the complete
|
||||
# formatted list through the spill backend below (ctx.spillStore, optional).
|
||||
|
||||
@@ -20,6 +20,13 @@ const SKILL_BLOCK_OPEN = '<skill name="scripted-skill">'
|
||||
const SKILL_BODY_MARKER = 'SCRIPTED SKILL BODY MARKER'
|
||||
const SKILL_RECEIVED_TEXT = 'Scripted skill body received.'
|
||||
const TITLE_TEXT = 'scripted session title'
|
||||
// The failing-bash scenario proves the terminal card reports a non-zero exit
|
||||
// exactly once: the model-facing result carries the `[exit code: N]` marker, and
|
||||
// the card turns it into its own `[exit N]` pill instead of showing both.
|
||||
const BASH_FAILURE_PROBE = 'Run the failing scripted command.'
|
||||
const BASH_FAILURE_COMMAND = 'printf "SCRIPTED_BASH_FAILED\\n"; exit 3'
|
||||
const BASH_FAILURE_TEXT = 'Scripted bash failure observed.'
|
||||
const BASH_FAILURE_CALL_ID = CallId('call-bash-failure')
|
||||
|
||||
function textChunks(text: string): StreamChunk[] {
|
||||
return [
|
||||
@@ -108,9 +115,23 @@ class ScriptedTuiAdapter extends LlmAdapter {
|
||||
return
|
||||
}
|
||||
|
||||
const hasToolResult = lastMessage?.content.some(block => block.type === 'tool-result') ?? false
|
||||
if (hasToolResult) {
|
||||
for (const chunk of textChunks(FINAL_TEXT)) yield chunk
|
||||
const blocks = lastMessage?.content ?? []
|
||||
if (blocks.some(block => block.type === 'tool-result')) {
|
||||
const answered = blocks.some(block => block.type === 'tool-result' && block.toolCallId === BASH_FAILURE_CALL_ID)
|
||||
for (const chunk of textChunks(answered ? BASH_FAILURE_TEXT : FINAL_TEXT)) yield chunk
|
||||
return
|
||||
}
|
||||
if (lastText.includes(BASH_FAILURE_PROBE)) {
|
||||
const bashArgs = JSON.stringify({ command: BASH_FAILURE_COMMAND, description: 'Run the failing scripted command' })
|
||||
yield { type: 'block-start', index: 0, blockType: 'tool-call' }
|
||||
yield { type: 'tool-call-delta', index: 0, id: BASH_FAILURE_CALL_ID, name: 'bash', argumentsDelta: bashArgs }
|
||||
yield {
|
||||
type: 'block-end',
|
||||
index: 0,
|
||||
block: { type: 'tool-call', id: BASH_FAILURE_CALL_ID, name: 'bash', arguments: bashArgs },
|
||||
}
|
||||
yield { type: 'usage', usage: { inputTokens: 20, outputTokens: 10 } }
|
||||
yield { type: 'finish', reason: { kind: 'tool-calls' } }
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -33,8 +33,6 @@
|
||||
# The smoke's log inspection reads plain `.jsonl`; keep the scripted
|
||||
# fixture uncompressed like the other snapshot-facing configs.
|
||||
persistenceCompression: none
|
||||
resumeSessionId: !!js "typeof resumeSessionId === 'string' ? resumeSessionId : undefined"
|
||||
resumeCommand: 'dsh --resume {session}'
|
||||
workspaceContext:
|
||||
maxBytes: 65536
|
||||
welcome: 'scripted TUI ready.'
|
||||
|
||||
@@ -365,11 +365,11 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
||||
expect(output).toContain('must be a top-level YAML array of loader patch entries')
|
||||
}, LOADER_SMOKE_TEST_TIMEOUT_MS)
|
||||
|
||||
it('routes the --resume flag into the config resume intake, failing loud on a missing id', async () => {
|
||||
// The flag path end to end: apps/cli parses `--resume missing-session` and
|
||||
// provides the id on the boot context, the shipped config's `!!js` reads it
|
||||
// as a bare identifier, and the resume fails loud — proving the printed
|
||||
// `dsh --resume <id>` hint reaches the config resume intake with no env var.
|
||||
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.
|
||||
const output = await smoke({
|
||||
label: 'dsh resume flag failure',
|
||||
tempDirPrefix: 'dsh-resume-flag-',
|
||||
@@ -380,6 +380,70 @@ describe('dsh CLI keyless smoke (apps/cli through the same PTY)', () => {
|
||||
expect(output).toContain('ui-tui: session "missing-session" failed to start:')
|
||||
}, LOADER_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
|
||||
// `--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)
|
||||
|
||||
it('keeps resume working when the personal overlay replaces the whole tui-agent config', async () => {
|
||||
// Loader patches replace a targeted `config` key wholesale, so a personal
|
||||
// overlay that omits a resume key used to silently disable the exit hint.
|
||||
// Launcher-owned identity and exit line make that unreachable.
|
||||
const output = await smoke({
|
||||
label: 'dsh overlay keeps resume',
|
||||
tempDirPrefix: 'dsh-overlay-resume-',
|
||||
binScript: dshBinScript,
|
||||
configArgs: [],
|
||||
prepare: seedWorkspace({
|
||||
personal: {
|
||||
'config.yaml': [
|
||||
'- id: tui-agent',
|
||||
" name: '@deepseek-ai/dsh-tui-demo'",
|
||||
' config:',
|
||||
' provider: deepseek',
|
||||
' model: deepseek-v4-flash',
|
||||
' workspaceContext: false',
|
||||
' 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({
|
||||
label: 'tui-agent bash exit pill',
|
||||
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)
|
||||
|
||||
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};
|
||||
// this test file sits an equal depth under the same root, so the same hop applies.
|
||||
|
||||
Reference in New Issue
Block a user