test(cli): cover the dsh built-bin non-TTY refusal
Removing the dsh-tui-demo bin dropped the only test of the TUI's piped-launch refusal. Add apps/cli/tests/built-bin.e2e.ts (apps/*/tests added to the e2e vitest include) running the built lib/bin.js under plain Node with piped stdio, and point the refusal message at `dsh -p "task"` for automation.
This commit is contained in:
+2
-2
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write
|
||||
2026-07-24-dsh-commander-argument-adapter.md: 60c47ef40cb0db833f7a2a526437b6a8ce812433
|
||||
2026-07-24-dsh-commander-argument-adapter.zh.md: 41a98499036c16330263d5072aa0fa454b892a24
|
||||
2026-07-24-dsh-commander-argument-adapter.md: c038f4facdc62039b8a31be5d660648fd81aebd2
|
||||
2026-07-24-dsh-commander-argument-adapter.zh.md: 285917af7c4b0da769ae7595bb1a6e5966adafd9
|
||||
@@ -22,7 +22,7 @@ Merging the concurrent safe-session-resume feature onto this parser retired the
|
||||
|
||||
## One terminal front door: `dsh`
|
||||
|
||||
The `dsh-tui-demo` package was a plugin (the TUI app bundle mounted by `dsh`'s config) plus a redundant `bin` that booted a leaf `cordis.yml` — the same job `dsh [config]` does. The bin is removed: `demo:cordis`, `demo:code-mode`, and both the tui-agent and cordis-agent keyless PTY smokes now launch through `apps/cli/src/bin.ts` with the config as the positional argument, and the package keeps only its plugin and invariant entries. The peer/dev `dsh-app-boot` dependency, the `bin`/`./bin` export, the `built-bin.e2e.ts` (its TUI piped-launch refusal is covered by `dsh`'s own TTY guard in the tui-agent PTY smoke), and the tsdown `bin` entry all leave with it. `cli-demo`, `acp-demo`, and `jsonrpc-demo` keep their bins because each is a distinct surface (headless, ACP, JSON-RPC) `dsh` does not provide.
|
||||
The `dsh-tui-demo` package was a plugin (the TUI app bundle mounted by `dsh`'s config) plus a redundant `bin` that booted a leaf `cordis.yml` — the same job `dsh [config]` does. The bin is removed: `demo:cordis`, `demo:code-mode`, and both the tui-agent and cordis-agent keyless PTY smokes now launch through `apps/cli/src/bin.ts` with the config as the positional argument, and the package keeps only its plugin and invariant entries. The peer/dev `dsh-app-boot` dependency, the `bin`/`./bin` export, the demo's `built-bin.e2e.ts`, and the tsdown `bin` entry all leave with it. `dsh`'s own TTY guard (refuse piped stdio before booting, pointing at `dsh -p` for automation) gains a matching `apps/cli/tests/built-bin.e2e.ts` that runs the built `lib/bin.js` under plain Node with piped stdio (`apps/*/tests` added to the e2e vitest include). `cli-demo`, `acp-demo`, and `jsonrpc-demo` keep their bins because each is a distinct surface (headless, ACP, JSON-RPC) `dsh` does not provide.
|
||||
|
||||
## Package topology
|
||||
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ argv 只在 `apps/cli/src/args.ts` 中解析一次,通过一个 Commander 适
|
||||
|
||||
## 唯一的终端入口:`dsh`
|
||||
|
||||
`dsh-tui-demo` 包(package)原本包含一个插件(即 `dsh` 配置挂载的 TUI 应用组合)和一个冗余的 `bin`;后者启动一份叶子配置 `cordis.yml`,所做的工作与 `dsh [config]` 相同。该 bin 已移除:`demo:cordis`、`demo:code-mode` 以及 tui-agent 和 cordis-agent 的两个无密钥 PTY 冒烟测试现在都通过 `apps/cli/src/bin.ts` 启动,并将配置作为位置参数;该包只保留插件入口和不变式入口。与该 bin 一同移除的还有对 `dsh-app-boot` 的对等依赖(peer dependency)和开发依赖、`bin` 和 `./bin` 导出、`built-bin.e2e.ts`(其中拒绝通过管道启动 TUI 的行为已由 tui-agent PTY 冒烟测试中 `dsh` 自身的 TTY 守卫覆盖),以及 tsdown 的 `bin` 入口。`cli-demo`、`acp-demo` 和 `jsonrpc-demo` 保留各自的 bin,因为它们分别提供 `dsh` 所没有的独立接口(headless、ACP(Agent Client Protocol)、JSON-RPC)。
|
||||
`dsh-tui-demo` 包(package)原本包含一个插件(即 `dsh` 配置挂载的 TUI 应用组合)和一个冗余的 `bin`;后者启动一份叶子配置 `cordis.yml`,所做的工作与 `dsh [config]` 相同。该 bin 已移除:`demo:cordis`、`demo:code-mode` 以及 tui-agent 和 cordis-agent 的两个无密钥 PTY 冒烟测试现在都通过 `apps/cli/src/bin.ts` 启动,并将配置作为位置参数;该包只保留插件入口和不变式入口。与该 bin 一同移除的还有对 `dsh-app-boot` 的对等依赖(peer dependency)和开发依赖、`bin` 和 `./bin` 导出、演示包的 `built-bin.e2e.ts`,以及 tsdown 的 `bin` 入口。`dsh` 自身的 TTY 守卫会在标准输入输出接入管道时,于启动应用前拒绝运行,并提示自动化场景改用 `dsh -p`;为此新增的 `apps/cli/tests/built-bin.e2e.ts` 将标准输入输出接入管道,直接使用 Node 运行构建后的 `lib/bin.js`(`apps/*/tests` 已加入 e2e Vitest 的测试文件匹配范围)。`cli-demo`、`acp-demo` 和 `jsonrpc-demo` 保留各自的 bin,因为它们分别提供 `dsh` 所没有的独立接口(headless、ACP(Agent Client Protocol)、JSON-RPC)。
|
||||
|
||||
## 包拓扑
|
||||
|
||||
|
||||
+3
-1
@@ -53,7 +53,9 @@ export async function runTui(config: string | undefined, resumeSessionId: string
|
||||
// is logged per-entry rather than rethrown, so a piped launch would
|
||||
// otherwise settle into an idle UI-less process instead of exiting nonzero.
|
||||
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
||||
process.stderr.write(`${NAME}: the TUI requires stdin and stdout to be interactive TTYs\n`)
|
||||
process.stderr.write(
|
||||
`${NAME}: the TUI requires stdin and stdout to be interactive TTYs; use \`${NAME} -p "task"\` for pipes and automation\n`,
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
installFailLoud(NAME)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
/**
|
||||
* Published-entry smoke for the `dsh` bin: run the built `lib/bin.js` under
|
||||
* plain Node (no tsx) with PIPED stdio and assert the TUI refuses to boot.
|
||||
* `dsh` is the sole terminal front door; the TUI owns no non-TTY fallback, so a
|
||||
* piped launch must exit nonzero with a stderr pointer at the one-shot `-p`
|
||||
* mode. The guard fires inside `runTui` BEFORE the Loader resolves the config
|
||||
* tree — a compose-time throw inside the tree is logged per-entry, not
|
||||
* rethrown, so without this guard a piped launch would settle into an idle
|
||||
* UI-less process. The bin resolves its workspace deps through the repo's
|
||||
* node_modules, so no external consumer is assembled; missing-config fail-loud
|
||||
* and full-boot coverage for the shared dsh-app-boot glue live in cli-demo's
|
||||
* built-bin suite, and interactive TTY behavior is PTY-covered by
|
||||
* examples/tui-agent. Skips before the bin is built.
|
||||
*/
|
||||
|
||||
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
|
||||
|
||||
/** Run the built bin with PIPED stdio; resolve with output + exit code. */
|
||||
function runBuiltBin(): Promise<{ stdout: string; code: number; stderr: string }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, [dshBin], { stdio: ['pipe', 'pipe', 'pipe'] })
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
child.stdout.setEncoding('utf8')
|
||||
child.stdout.on('data', (c: string) => { stdout += c })
|
||||
child.stderr.setEncoding('utf8')
|
||||
child.stderr.on('data', (c: string) => { stderr += c })
|
||||
const timer = setTimeout(() => {
|
||||
child.kill('SIGKILL')
|
||||
reject(new Error(`dsh built bin did not exit within 25s. stdout:\n${stdout}\nstderr:\n${stderr}`))
|
||||
}, 25_000)
|
||||
child.on('exit', (code) => { clearTimeout(timer); resolve({ stdout, code: code ?? -1, stderr }) })
|
||||
child.on('error', (err) => { clearTimeout(timer); reject(err) })
|
||||
child.stdin.end()
|
||||
})
|
||||
}
|
||||
|
||||
describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
|
||||
it('refuses pipes LOUD (non-zero exit + stderr) before booting the Loader', async () => {
|
||||
const { stdout, code, stderr } = await runBuiltBin()
|
||||
expect(code).not.toBe(0)
|
||||
expect(stderr).toContain('requires stdin and stdout to be interactive TTYs')
|
||||
expect(stderr).toContain('dsh -p')
|
||||
// The refusal happens before any plugin mounts: stdout stays silent.
|
||||
expect(stdout).toBe('')
|
||||
}, 30_000)
|
||||
})
|
||||
@@ -38,7 +38,7 @@ export default defineConfig({
|
||||
plugins: [tsconfigPaths({ projects: ['./tsconfig.base.json'] })],
|
||||
test: {
|
||||
setupFiles: ['./scripts/test-invariants.ts'],
|
||||
include: ['packages/*/*/tests/**/*.e2e.ts', 'examples/*/tests/**/*.e2e.ts'],
|
||||
include: ['packages/*/*/tests/**/*.e2e.ts', 'apps/*/tests/**/*.e2e.ts', 'examples/*/tests/**/*.e2e.ts'],
|
||||
// Real model calls: generous timeouts, and retries for transient flakes
|
||||
// (the shared internal key hits concurrency quotas). No coverage — the
|
||||
// unit suites own the coverage gate.
|
||||
|
||||
Reference in New Issue
Block a user