built-bin.e2e.ts 2.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
  1. import { existsSync } from 'node:fs'
  2. import { join } from 'node:path'
  3. import { fileURLToPath } from 'node:url'
  4. import { execa } from 'execa'
  5. import { describe, expect, it } from 'vitest'
  6. /**
  7. * Published-entry smoke for the `dsh` bin: run the built `lib/bin.js` under
  8. * plain Node (no tsx) with PIPED stdio and assert the TUI refuses to boot.
  9. * `dsh` is the sole terminal front door; the TUI owns no non-TTY fallback, so a
  10. * piped launch must exit nonzero with a stderr pointer at the one-shot `-p`
  11. * mode. The guard fires inside `runTui` BEFORE the Loader resolves the config
  12. * tree — a compose-time throw inside the tree is logged per-entry, not
  13. * rethrown, so without this guard a piped launch would settle into an idle
  14. * UI-less process. The bin resolves its workspace deps through the repo's
  15. * node_modules, so no external consumer is assembled; missing-config fail-loud
  16. * and full-boot coverage for the shared dsh-app-boot glue live in cli-demo's
  17. * built-bin suite, and interactive TTY behavior is PTY-covered by
  18. * examples/tui-agent. Skips before the bin is built.
  19. */
  20. const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
  21. const dshBin = join(repoRoot, 'apps/cli/lib/bin.js')
  22. /** Run the built bin with PIPED stdio (stdin closed at EOF); resolve with output + exit code. */
  23. async function runBuiltBin(): Promise<{ stdout: string; code: number; stderr: string }> {
  24. const result = await execa(process.execPath, [dshBin], {
  25. input: '',
  26. timeout: 25_000,
  27. killSignal: 'SIGKILL',
  28. reject: false,
  29. })
  30. if (result.timedOut) {
  31. throw new Error(`dsh built bin did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`)
  32. }
  33. return { stdout: result.stdout, code: result.exitCode ?? -1, stderr: result.stderr }
  34. }
  35. describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', () => {
  36. it('refuses pipes LOUD (non-zero exit + stderr) before booting the Loader', async () => {
  37. const { stdout, code, stderr } = await runBuiltBin()
  38. expect(code).not.toBe(0)
  39. expect(stderr).toContain('requires stdin and stdout to be interactive TTYs')
  40. expect(stderr).toContain('dsh -p')
  41. // The refusal happens before any plugin mounts: stdout stays silent.
  42. expect(stdout).toBe('')
  43. }, 30_000)
  44. })