{"version":3,"file":"test.mjs","names":[],"sources":["../../src/feature/test.ts"],"sourcesContent":["import type { InteractivePromptConfig, PadroneRuntime } from '../core/runtime.ts';\nimport type { AnyPadroneCommand, PadroneCommandResult } from '../types/index.ts';\n\n/**\n * Result from a single command execution in test mode.\n * Extends the standard PadroneCommandResult with captured I/O.\n */\nexport type TestCliResult = {\n  /** The matched command. */\n  command: AnyPadroneCommand;\n  /** Validated arguments (undefined if validation failed). */\n  args: unknown;\n  /** Action handler return value (undefined if validation failed or no action). */\n  result: unknown;\n  /** Validation issues, if any. */\n  issues: { message: string; path?: PropertyKey[] }[] | undefined;\n  /** All values passed to `runtime.output()`. */\n  stdout: unknown[];\n  /** All strings passed to `runtime.error()`. */\n  stderr: string[];\n  /** The thrown error, if the command threw (routing error, action error, etc.). */\n  error?: unknown;\n};\n\n/**\n * Result from a REPL test session.\n */\nexport type TestReplResult = {\n  /** One entry per successfully executed command (validation errors are captured in stderr, not here). */\n  results: Omit<TestCliResult, 'stdout' | 'stderr'>[];\n  /** All output from the entire REPL session. */\n  stdout: unknown[];\n  /** All errors from the entire REPL session. */\n  stderr: string[];\n};\n\n/**\n * Fluent builder for setting up CLI test scenarios.\n */\nexport type TestCliBuilder = {\n  /** Set the CLI input string (e.g. `'deploy --env production'`). */\n  args(input: string): TestCliBuilder;\n  /** Set environment variables visible to the command. */\n  env(vars: Record<string, string | undefined>): TestCliBuilder;\n  /** Provide mock answers for interactive prompts. Keys are field names. */\n  prompt(answers: Record<string, unknown>): TestCliBuilder;\n  /** Provide mock stdin data (simulates piped input). */\n  stdin(data: string): TestCliBuilder;\n  /**\n   * Execute a single command via `eval()` and return the result with captured I/O.\n   * @param input - Optional CLI input string. Overrides `.args()` if provided.\n   */\n  run(input?: string): Promise<TestCliResult>;\n  /**\n   * Run a REPL session with the given sequence of inputs.\n   * Each string in the array is fed as one line of input.\n   * The session ends after all inputs are consumed (EOF).\n   */\n  repl(inputs: string[]): Promise<TestReplResult>;\n};\n\n/**\n * Creates a fluent test builder for a Padrone program.\n * Captures all I/O and provides a clean interface for assertions.\n *\n * Works with any test framework (bun:test, vitest, jest, node:test, etc.).\n *\n * @example\n * ```ts\n * import { testCli } from 'padrone/test'\n *\n * const result = await testCli(myProgram)\n *   .args('deploy --env production')\n *   .env({ API_KEY: 'xxx' })\n *   .run()\n *\n * expect(result.result).toBe('Deployed')\n * expect(result.stdout).toContain('Deploying...')\n * ```\n *\n * @example\n * ```ts\n * // Shorthand: pass input directly to run()\n * const result = await testCli(myProgram).run('deploy --env production')\n * ```\n *\n * @example\n * ```ts\n * // Test interactive prompts\n * const result = await testCli(myProgram)\n *   .args('init')\n *   .prompt({ name: 'myapp', template: 'react' })\n *   .run()\n *\n * expect(result.args).toEqual({ name: 'myapp', template: 'react' })\n * ```\n *\n * @example\n * ```ts\n * // Test REPL sessions\n * const { results } = await testCli(myProgram)\n *   .repl(['greet World', 'add --a=2 --b=3'])\n *\n * expect(results[0].result).toBe('Hello, World!')\n * expect(results[1].result).toBe(5)\n * ```\n */\n/**\n * Any program-like object that has `eval`, `runtime`, and `repl` methods.\n * Avoids strict variance issues with `AnyPadroneProgram`.\n */\ntype TestableProgram = {\n  eval: (input: string, prefs?: Record<string, unknown>) => any;\n  runtime: (runtime: PadroneRuntime) => TestableProgram;\n  repl: (options?: { greeting?: false; hint?: false }) => AsyncIterable<any>;\n};\n\nexport function testCli(program: TestableProgram): TestCliBuilder {\n  let input: string | undefined;\n  let envVars: Record<string, string | undefined> | undefined;\n  let promptAnswers: Record<string, unknown> | undefined;\n  let stdinData: string | undefined;\n\n  const builder: TestCliBuilder = {\n    args(args: string) {\n      input = args;\n      return builder;\n    },\n    env(vars) {\n      envVars = vars;\n      return builder;\n    },\n    prompt(answers) {\n      promptAnswers = answers;\n      return builder;\n    },\n    stdin(data: string) {\n      stdinData = data;\n      return builder;\n    },\n\n    async run(runInput?: string) {\n      const stdout: unknown[] = [];\n      const stderr: string[] = [];\n\n      const runtime = buildRuntime(stdout, stderr, { envVars, promptAnswers, stdinData });\n      const testProgram = program.runtime(runtime);\n\n      const evalResult = await testProgram.eval(runInput ?? input ?? '', {});\n      if (evalResult.error) {\n        stderr.push(evalResult.error instanceof Error ? evalResult.error.message : String(evalResult.error));\n      }\n      return toTestResult(evalResult, stdout, stderr);\n    },\n\n    async repl(inputs: string[]) {\n      const stdout: unknown[] = [];\n      const stderr: string[] = [];\n\n      const runtime = buildRuntime(stdout, stderr, {\n        envVars,\n        promptAnswers,\n        readLine: createMockReadLine(inputs),\n      });\n\n      const testProgram = program.runtime(runtime);\n      const results: Omit<TestCliResult, 'stdout' | 'stderr'>[] = [];\n\n      for await (const r of testProgram.repl({ greeting: false, hint: false })) {\n        results.push({\n          command: r.command!,\n          args: r.args,\n          result: r.result,\n          issues: r.argsResult?.issues as TestCliResult['issues'],\n        });\n      }\n\n      return { results, stdout, stderr };\n    },\n  };\n\n  return builder;\n}\n\nfunction toTestResult(evalResult: PadroneCommandResult, stdout: unknown[], stderr: string[]): TestCliResult {\n  return {\n    command: evalResult.command!,\n    args: evalResult.args,\n    result: evalResult.result,\n    error: evalResult.error,\n    issues: evalResult.argsResult?.issues as TestCliResult['issues'],\n    stdout,\n    stderr,\n  };\n}\n\nfunction buildRuntime(\n  stdout: unknown[],\n  stderr: string[],\n  opts: {\n    envVars?: Record<string, string | undefined>;\n    promptAnswers?: Record<string, unknown>;\n    readLine?: (prompt: string) => Promise<string | null>;\n    stdinData?: string;\n  },\n): PadroneRuntime {\n  const runtime: PadroneRuntime = {\n    output: (...args: unknown[]) => stdout.push(...args),\n    error: (text: string) => stderr.push(text),\n  };\n\n  if (opts.envVars) {\n    runtime.env = () => opts.envVars!;\n  }\n\n  if (opts.promptAnswers) {\n    runtime.interactive = 'supported';\n    runtime.prompt = async (config: InteractivePromptConfig) => opts.promptAnswers![config.name];\n  }\n\n  if (opts.readLine) {\n    runtime.readLine = opts.readLine;\n  }\n\n  if (opts.stdinData !== undefined) {\n    runtime.stdin = {\n      isTTY: false,\n      async text() {\n        return opts.stdinData!;\n      },\n      async *lines() {\n        const lines = opts.stdinData!.split('\\n');\n        // Remove trailing empty line from final newline (matches readline behavior)\n        if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();\n        for (const line of lines) {\n          yield line;\n        }\n      },\n    };\n  } else {\n    // No stdin data: simulate a TTY (no piped input) to avoid reading from process.stdin\n    runtime.stdin = {\n      isTTY: true,\n      async text() {\n        return '';\n      },\n      async *lines() {\n        // no lines\n      },\n    };\n  }\n\n  return runtime;\n}\n\nfunction createMockReadLine(inputs: string[]): (prompt: string) => Promise<string | null> {\n  let index = 0;\n  return async (_prompt: string): Promise<string | null> => {\n    if (index >= inputs.length) return null;\n    return inputs[index++] ?? null;\n  };\n}\n"],"mappings":";AAqHA,SAAgB,QAAQ,SAA0C;CAChE,IAAI;CACJ,IAAI;CACJ,IAAI;CACJ,IAAI;CAEJ,MAAM,UAA0B;EAC9B,KAAK,MAAc;GACjB,QAAQ;GACR,OAAO;EACT;EACA,IAAI,MAAM;GACR,UAAU;GACV,OAAO;EACT;EACA,OAAO,SAAS;GACd,gBAAgB;GAChB,OAAO;EACT;EACA,MAAM,MAAc;GAClB,YAAY;GACZ,OAAO;EACT;EAEA,MAAM,IAAI,UAAmB;GAC3B,MAAM,SAAoB,CAAC;GAC3B,MAAM,SAAmB,CAAC;GAE1B,MAAM,UAAU,aAAa,QAAQ,QAAQ;IAAE;IAAS;IAAe;GAAU,CAAC;GAGlF,MAAM,aAAa,MAFC,QAAQ,QAAQ,OAED,CAAC,CAAC,KAAK,YAAY,SAAS,IAAI,CAAC,CAAC;GACrE,IAAI,WAAW,OACb,OAAO,KAAK,WAAW,iBAAiB,QAAQ,WAAW,MAAM,UAAU,OAAO,WAAW,KAAK,CAAC;GAErG,OAAO,aAAa,YAAY,QAAQ,MAAM;EAChD;EAEA,MAAM,KAAK,QAAkB;GAC3B,MAAM,SAAoB,CAAC;GAC3B,MAAM,SAAmB,CAAC;GAE1B,MAAM,UAAU,aAAa,QAAQ,QAAQ;IAC3C;IACA;IACA,UAAU,mBAAmB,MAAM;GACrC,CAAC;GAED,MAAM,cAAc,QAAQ,QAAQ,OAAO;GAC3C,MAAM,UAAsD,CAAC;GAE7D,WAAW,MAAM,KAAK,YAAY,KAAK;IAAE,UAAU;IAAO,MAAM;GAAM,CAAC,GACrE,QAAQ,KAAK;IACX,SAAS,EAAE;IACX,MAAM,EAAE;IACR,QAAQ,EAAE;IACV,QAAQ,EAAE,YAAY;GACxB,CAAC;GAGH,OAAO;IAAE;IAAS;IAAQ;GAAO;EACnC;CACF;CAEA,OAAO;AACT;AAEA,SAAS,aAAa,YAAkC,QAAmB,QAAiC;CAC1G,OAAO;EACL,SAAS,WAAW;EACpB,MAAM,WAAW;EACjB,QAAQ,WAAW;EACnB,OAAO,WAAW;EAClB,QAAQ,WAAW,YAAY;EAC/B;EACA;CACF;AACF;AAEA,SAAS,aACP,QACA,QACA,MAMgB;CAChB,MAAM,UAA0B;EAC9B,SAAS,GAAG,SAAoB,OAAO,KAAK,GAAG,IAAI;EACnD,QAAQ,SAAiB,OAAO,KAAK,IAAI;CAC3C;CAEA,IAAI,KAAK,SACP,QAAQ,YAAY,KAAK;CAG3B,IAAI,KAAK,eAAe;EACtB,QAAQ,cAAc;EACtB,QAAQ,SAAS,OAAO,WAAoC,KAAK,cAAe,OAAO;CACzF;CAEA,IAAI,KAAK,UACP,QAAQ,WAAW,KAAK;CAG1B,IAAI,KAAK,cAAc,KAAA,GACrB,QAAQ,QAAQ;EACd,OAAO;EACP,MAAM,OAAO;GACX,OAAO,KAAK;EACd;EACA,OAAO,QAAQ;GACb,MAAM,QAAQ,KAAK,UAAW,MAAM,IAAI;GAExC,IAAI,MAAM,SAAS,KAAK,MAAM,MAAM,SAAS,OAAO,IAAI,MAAM,IAAI;GAClE,KAAK,MAAM,QAAQ,OACjB,MAAM;EAEV;CACF;MAGA,QAAQ,QAAQ;EACd,OAAO;EACP,MAAM,OAAO;GACX,OAAO;EACT;EACA,OAAO,QAAQ,CAEf;CACF;CAGF,OAAO;AACT;AAEA,SAAS,mBAAmB,QAA8D;CACxF,IAAI,QAAQ;CACZ,OAAO,OAAO,YAA4C;EACxD,IAAI,SAAS,OAAO,QAAQ,OAAO;EACnC,OAAO,OAAO,YAAY;CAC5B;AACF"}