process.ts 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
  1. // ---------------------------------------------------------------------------
  2. // Process-level helpers for headless / contract-style CLI tests.
  3. //
  4. // These helpers spawn cline as a child process and return stdout, stderr,
  5. // and exit code — without going through the TUI harness. Use these for:
  6. // - Exit code assertions
  7. // - Pure stdout/stderr contract tests
  8. // - Piped stdin tests (echo "..." | cline -y ...)
  9. // - JSON output validation
  10. // - Timeout behavior
  11. // ---------------------------------------------------------------------------
  12. import { SpawnSyncOptions, spawnSync } from "child_process"
  13. import { CLINE_BIN } from "./constants.js"
  14. import { clineEnv } from "./env.js"
  15. export interface RunResult {
  16. stdout: string
  17. stderr: string
  18. exitCode: number | null
  19. combined: string
  20. }
  21. export interface RunOptions {
  22. /** Named config dir under `configs/`, or absolute path */
  23. config?: string
  24. /** Additional env vars */
  25. env?: NodeJS.ProcessEnv
  26. /** Stdin to pipe into the process */
  27. stdin?: string
  28. /** Timeout in ms (default: 30_000) */
  29. timeout?: number
  30. /** Working directory for the spawned process */
  31. cwd?: string
  32. }
  33. /**
  34. * Run `cline [args]` synchronously and return stdout/stderr/exitCode.
  35. *
  36. * Suitable for deterministic, fast-exiting commands like:
  37. * cline --help, cline --version, cline auth -p ... -k ..., etc.
  38. */
  39. export function runCline(args: string[], opts: RunOptions = {}): RunResult {
  40. const { config = "default", env: extraEnv = {}, stdin, timeout = 30_000, cwd = process.cwd() } = opts
  41. const spawnOpts: SpawnSyncOptions = {
  42. encoding: "utf8",
  43. timeout,
  44. cwd,
  45. env: { ...clineEnv(config), ...extraEnv },
  46. input: stdin,
  47. }
  48. const result = spawnSync(CLINE_BIN, args, spawnOpts)
  49. const stdout = (result.stdout as string) ?? ""
  50. const stderr = (result.stderr as string) ?? ""
  51. return {
  52. stdout,
  53. stderr,
  54. exitCode: result.status,
  55. combined: stdout + stderr,
  56. }
  57. }
  58. /**
  59. * Assert that a runCline result exited with the expected code.
  60. * Throws a descriptive error if the code doesn't match.
  61. */
  62. export function assertExitCode(result: RunResult, expected: number, label = ""): void {
  63. if (result.exitCode !== expected) {
  64. const prefix = label ? `[${label}] ` : ""
  65. throw new Error(
  66. `${prefix}Expected exit code ${expected}, got ${result.exitCode}.\n` +
  67. `stdout: ${result.stdout}\n` +
  68. `stderr: ${result.stderr}`,
  69. )
  70. }
  71. }
  72. /**
  73. * Assert that stdout/stderr contains a given string or matches a regex.
  74. */
  75. export function assertOutput(
  76. result: RunResult,
  77. pattern: string | RegExp,
  78. stream: "stdout" | "stderr" | "combined" = "combined",
  79. label = "",
  80. ): void {
  81. const text = result[stream]
  82. const matches = typeof pattern === "string" ? text.includes(pattern) : pattern.test(text)
  83. if (!matches) {
  84. const prefix = label ? `[${label}] ` : ""
  85. throw new Error(
  86. `${prefix}Expected ${stream} to match ${pattern}.\n` + `stdout: ${result.stdout}\n` + `stderr: ${result.stderr}`,
  87. )
  88. }
  89. }