{"version":3,"sources":["../../src/xcode/platform.ts","../../src/scan/command.ts","../../src/scan/retry.ts","../../src/scan/index.ts"],"names":["join","mkdirSync","existsSync","spawn"],"mappings":";;;;;;;;;AAAO,SAAS,aAAa,OAAA,EAAuB;AAClD,EAAA,IAAI,OAAA,CAAQ,aAAa,QAAA,EAAU;AACjC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR,CAAA,EAAG,OAAO,CAAA,yFAAA,EACoB,OAAA,CAAQ,QAAQ,CAAA,CAAA;AAAA,KAChD;AAAA,EACF;AACF;;;ACuBO,SAAS,iBAAiB,OAAA,EAAuC;AACtE,EAAA,MAAM,OAAiB,EAAC;AAExB,EAAA,IAAI,OAAA,CAAQ,KAAA,EAAO,IAAA,CAAK,IAAA,CAAK,OAAO,CAAA;AACpC,EAAA,IAAA,CAAK,KAAK,MAAM,CAAA;AAEhB,EAAA,IAAI,QAAQ,SAAA,EAAW,IAAA,CAAK,IAAA,CAAK,YAAA,EAAc,QAAQ,SAAS,CAAA;AAAA,OAAA,IACvD,QAAQ,OAAA,EAAS,IAAA,CAAK,IAAA,CAAK,UAAA,EAAY,QAAQ,OAAO,CAAA;AAE/D,EAAA,IAAI,QAAQ,MAAA,EAAQ,IAAA,CAAK,IAAA,CAAK,SAAA,EAAW,QAAQ,MAAM,CAAA;AAGvD,EAAA,IAAI,QAAQ,WAAA,EAAa;AACvB,IAAA,IAAA,CAAK,IAAA,CAAK,cAAA,EAAgB,OAAA,CAAQ,WAAW,CAAA;AAAA,EAC/C,WAAW,OAAA,CAAQ,OAAA,IAAW,OAAA,CAAQ,OAAA,CAAQ,SAAS,CAAA,EAAG;AACxD,IAAA,KAAA,MAAW,MAAA,IAAU,QAAQ,OAAA,EAAS;AACpC,MAAA,IAAA,CAAK,IAAA,CAAK,cAAA,EAAgB,CAAA,4BAAA,EAA+B,MAAM,CAAA,CAAE,CAAA;AAAA,IACnE;AAAA,EACF;AAEA,EAAA,IAAI,QAAQ,QAAA,EAAU,IAAA,CAAK,IAAA,CAAK,WAAA,EAAa,QAAQ,QAAQ,CAAA;AAC7D,EAAA,IAAI,QAAQ,aAAA,EAAe,IAAA,CAAK,IAAA,CAAK,gBAAA,EAAkB,QAAQ,aAAa,CAAA;AAC5E,EAAA,IAAI,QAAQ,eAAA,EAAiB,IAAA,CAAK,IAAA,CAAK,kBAAA,EAAoB,QAAQ,eAAe,CAAA;AAClF,EAAA,IAAI,QAAQ,gBAAA,EAAkB,IAAA,CAAK,IAAA,CAAK,mBAAA,EAAqB,QAAQ,gBAAgB,CAAA;AAErF,EAAA,IAAI,OAAA,CAAQ,YAAA,EAAc,IAAA,CAAK,IAAA,CAAK,uBAAuB,KAAK,CAAA;AAEhE,EAAA,IAAI,QAAQ,WAAA,EAAa;AACvB,IAAA,KAAA,MAAW,IAAA,IAAQ,QAAQ,WAAA,EAAa;AACtC,MAAA,IAAA,CAAK,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAAA,IACnC;AAAA,EACF;AACA,EAAA,IAAI,QAAQ,WAAA,EAAa;AACvB,IAAA,KAAA,MAAW,IAAA,IAAQ,QAAQ,WAAA,EAAa;AACtC,MAAA,IAAA,CAAK,IAAA,CAAK,mBAAmB,IAAI,CAAA;AAAA,IACnC;AAAA,EACF;AAGA,EAAA,IAAI,OAAA,CAAQ,oBAAoB,MAAA,EAAW;AACzC,IAAA,IAAA,CAAK,IAAA,CAAK,2BAAA,EAA6B,OAAA,CAAQ,eAAA,GAAkB,QAAQ,IAAI,CAAA;AAAA,EAC/E;AACA,EAAA,IAAI,OAAA,CAAQ,sBAAsB,MAAA,EAAW;AAC3C,IAAA,IAAA,CAAK,IAAA,CAAK,gCAAA,EAAkC,MAAA,CAAO,OAAA,CAAQ,iBAAiB,CAAC,CAAA;AAAA,EAC/E;AACA,EAAA,IAAI,OAAA,CAAQ,4BAA4B,MAAA,EAAW;AACjD,IAAA,IAAA,CAAK,IAAA;AAAA,MACH,iDAAA;AAAA,MACA,MAAA,CAAO,QAAQ,uBAAuB;AAAA,KACxC;AAAA,EACF;AACA,EAAA,IAAI,QAAQ,wBAAA,EAA0B;AACpC,IAAA,IAAA,CAAK,KAAK,yCAAyC,CAAA;AAAA,EACrD;AAGA,EAAA,IAAI,OAAA,CAAQ,qBAAqB,MAAA,EAAW;AAC1C,IAAA,IAAA,CAAK,IAAA,CAAK,yBAAA,EAA2B,OAAA,CAAQ,gBAAA,GAAmB,QAAQ,IAAI,CAAA;AAAA,EAC9E;AACA,EAAA,IAAI,OAAA,CAAQ,oBAAoB,MAAA,EAAW;AACzC,IAAA,IAAA,CAAK,IAAA,CAAK,wBAAA,EAA0B,OAAA,CAAQ,eAAA,GAAkB,QAAQ,IAAI,CAAA;AAAA,EAC5E;AACA,EAAA,IAAI,OAAA,CAAQ,+BAA+B,MAAA,EAAW;AACpD,IAAA,IAAA,CAAK,IAAA;AAAA,MACH,mCAAA;AAAA,MACA,OAAA,CAAQ,6BAA6B,KAAA,GAAQ;AAAA,KAC/C;AAAA,EACF;AAEA,EAAA,IAAI,OAAA,CAAQ,QAAQ,IAAA,CAAK,IAAA,CAAK,GAAG,OAAA,CAAQ,MAAA,CAAO,KAAA,CAAM,KAAK,CAAC,CAAA;AAE5D,EAAA,OAAO,IAAA;AACT;;;AC/FO,SAAS,iBAAiB,MAAA,EAA8B;AAC7D,EAAA,MAAM,UAAwB,EAAC;AAE/B,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,OAAA,CAAQ,iBAAA,EAAmB,EAAE,CAAA;AAGlD,EAAA,MAAM,YAAA,GAAe,KAAA,CAAM,KAAA,CAAM,gDAAgD,CAAA;AACjF,EAAA,IAAI,CAAC,cAAc,OAAO,OAAA;AAE1B,EAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,CAAC,CAAA,CAAE,MAAM,IAAI,CAAA;AACxC,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,EAAK;AAC1B,IAAA,IAAI,CAAC,OAAA,IAAW,OAAA,CAAQ,UAAA,CAAW,GAAG,CAAA,EAAG;AAGzC,IAAA,MAAM,YAAA,GAAe,OAAA,CAAQ,KAAA,CAAM,oBAAoB,CAAA;AACvD,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,QACX,MAAA,EAAQ,EAAA;AAAA,QACR,KAAA,EAAO,aAAa,CAAC,CAAA;AAAA,QACrB,QAAA,EAAU,aAAa,CAAC,CAAA;AAAA,QACxB,cAAA,EAAgB,GAAG,YAAA,CAAa,CAAC,CAAC,CAAA,CAAA,EAAI,YAAA,CAAa,CAAC,CAAC,CAAA;AAAA,OACtD,CAAA;AACD,MAAA;AAAA,IACF;AAEA,IAAA,MAAM,UAAA,GAAa,OAAA,CAAQ,KAAA,CAAM,uBAAuB,CAAA;AACxD,IAAA,IAAI,UAAA,EAAY;AACd,MAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,QACX,MAAA,EAAQ,WAAW,CAAC,CAAA;AAAA,QACpB,KAAA,EAAO,WAAW,CAAC,CAAA;AAAA,QACnB,QAAA,EAAU,WAAW,CAAC,CAAA;AAAA,QACtB,cAAA,EAAgB,CAAA,EAAG,UAAA,CAAW,CAAC,CAAC,CAAA,CAAA,EAAI,UAAA,CAAW,CAAC,CAAC,CAAA,CAAA,EAAI,UAAA,CAAW,CAAC,CAAC,CAAA;AAAA,OACnE,CAAA;AAAA,IACH;AAAA,EACF;AAEA,EAAA,OAAO,OAAA;AACT;;;ACJA,eAAsB,SAAS,OAAA,EAA2C;AACxE,EAAA,YAAA,CAAa,MAAM,CAAA;AAEnB,EAAA,MAAM,YAAY,OAAA,CAAQ,eAAA,IAAmBA,UAAK,OAAA,CAAQ,GAAA,IAAO,aAAa,CAAA;AAC9E,EAAAC,YAAA,CAAU,SAAA,EAAW,EAAE,SAAA,EAAW,IAAA,EAAM,CAAA;AAExC,EAAA,MAAM,gBAAA,GAAmB,OAAA,CAAQ,gBAAA,IAAoBD,SAAA,CAAK,WAAW,iBAAiB,CAAA;AACtF,EAAA,MAAM,UAAA,GAAa,QAAQ,UAAA,IAAc,CAAA;AACzC,EAAA,IAAI,UAAA,GAAa,CAAA;AACjB,EAAA,IAAI,UAAA,GAAa,EAAA;AACjB,EAAA,IAAI,qBAAqB,OAAA,CAAQ,WAAA;AAEjC,EAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAE3B,EAAA,KAAA,IAAS,OAAA,GAAU,CAAA,EAAG,OAAA,IAAW,UAAA,EAAY,OAAA,EAAA,EAAW;AACtD,IAAA,MAAM,OAAO,gBAAA,CAAiB;AAAA,MAC5B,GAAG,OAAA;AAAA,MACH,kBAAkB,OAAA,KAAY,CAAA,GAAI,mBAAmB,CAAA,EAAG,gBAAgB,UAAU,OAAO,CAAA,CAAA;AAAA,MACzF,WAAA,EAAa;AAAA,KACd,CAAA;AAED,IAAA,MAAM,MAAA,GAAS,MAAM,aAAA,CAAc,IAAA,EAAM,QAAQ,QAAQ,CAAA;AACzD,IAAA,UAAA,GAAa,MAAA,CAAO,MAAA;AAEpB,IAAA,IAAI,MAAA,CAAO,aAAa,CAAA,EAAG;AAEzB,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,UAAU,UAAA,EAAY;AAExB,MAAA,MAAM,MAAA,GAAS,gBAAA,CAAiB,MAAA,CAAO,MAAM,CAAA;AAC7C,MAAA,IAAI,MAAA,CAAO,WAAW,CAAA,EAAG;AAEzB,MAAA,kBAAA,GAAqB,MAAA,CAAO,GAAA,CAAI,CAAC,CAAA,KAAM,EAAE,cAAc,CAAA;AACvD,MAAA,UAAA,EAAA;AAAA,IACF;AAAA,EACF;AAGA,EAAA,MAAM,MAAA,GAAS,gBAAgB,UAAU,CAAA;AACzC,EAAA,MAAM,QAAA,GAAA,CAAY,IAAA,CAAK,GAAA,EAAI,GAAI,SAAA,IAAa,GAAA;AAE5C,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,OAAO,QAAA,KAAa,CAAA;AAAA,IAC7B,YAAY,MAAA,CAAO,KAAA;AAAA,IACnB,UAAU,MAAA,CAAO,QAAA;AAAA,IACjB,SAAS,MAAA,CAAO,OAAA;AAAA,IAChB,OAAA,EAAS,UAAA;AAAA,IACT,QAAA;AAAA,IACA,aAAaE,aAAA,CAAW,gBAAgB,IAAI,CAAC,gBAAgB,IAAI,EAAC;AAAA,IAClE,MAAA,EAAQ;AAAA,GACV;AACF;AAEA,SAAS,aAAA,CACP,MACA,QAAA,EAC+C;AAC/C,EAAA,OAAO,IAAI,OAAA,CAAQ,CAAC,OAAA,KAAY;AAC9B,IAAA,MAAM,KAAA,GAAQC,mBAAA,CAAM,YAAA,EAAc,IAAA,EAAM,EAAE,KAAA,EAAO,CAAC,QAAA,EAAU,MAAA,EAAQ,MAAM,CAAA,EAAG,CAAA;AAC7E,IAAA,IAAI,MAAA,GAAS,EAAA;AAEb,IAAA,KAAA,CAAM,MAAA,CAAO,EAAA,CAAG,MAAA,EAAQ,CAAC,IAAA,KAAiB;AACxC,MAAA,MAAM,KAAA,GAAQ,KAAK,QAAA,EAAS;AAC5B,MAAA,MAAA,IAAU,KAAA;AACV,MAAA,QAAA,GAAW,KAAK,CAAA;AAAA,IAClB,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,MAAA,CAAO,EAAA,CAAG,MAAA,EAAQ,CAAC,IAAA,KAAiB;AACxC,MAAA,MAAM,KAAA,GAAQ,KAAK,QAAA,EAAS;AAC5B,MAAA,MAAA,IAAU,KAAA;AACV,MAAA,QAAA,GAAW,KAAK,CAAA;AAAA,IAClB,CAAC,CAAA;AACD,IAAA,KAAA,CAAM,EAAA,CAAG,OAAA,EAAS,CAAC,IAAA,KAAS,OAAA,CAAQ,EAAE,QAAA,EAAU,IAAA,IAAQ,CAAA,EAAG,MAAA,EAAQ,CAAC,CAAA;AAAA,EACtE,CAAC,CAAA;AACH;AAEA,SAAS,gBAAgB,MAAA,EAAsE;AAE7F,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,KAAA,CAAM,6CAA6C,CAAA;AACxE,EAAA,IAAI,KAAA,EAAO;AACT,IAAA,OAAO;AAAA,MACL,KAAA,EAAO,QAAA,CAAS,KAAA,CAAM,CAAC,GAAG,EAAE,CAAA;AAAA,MAC5B,QAAA,EAAU,QAAA,CAAS,KAAA,CAAM,CAAC,GAAG,EAAE,CAAA;AAAA,MAC/B,OAAA,EAAS;AAAA,KACX;AAAA,EACF;AACA,EAAA,OAAO,EAAE,KAAA,EAAO,CAAA,EAAG,QAAA,EAAU,CAAA,EAAG,SAAS,CAAA,EAAE;AAC7C","file":"index.cjs","sourcesContent":["export function requireMacOS(feature: string): void {\n  if (process.platform !== \"darwin\") {\n    throw new Error(\n      `${feature} requires macOS. This operation uses Xcode command-line tools ` +\n      `which are not available on ${process.platform}.`\n    );\n  }\n}\n\nexport function isMacOS(): boolean {\n  return process.platform === \"darwin\";\n}\n","export interface TestCommandOptions {\n  scheme?: string;\n  workspace?: string;\n  project?: string;\n  destination?: string;\n  devices?: string[];\n  testPlan?: string;\n  onlyTesting?: string[];\n  skipTesting?: string[];\n  configuration?: string;\n  derivedDataPath?: string;\n  resultBundlePath?: string;\n  codeCoverage?: boolean;\n  xcargs?: string;\n  clean?: boolean;\n  // ── Parallel testing (Fastlane scan parity) ──────────────────────────────\n  /** Enable `-parallel-testing-enabled YES`. */\n  parallelTesting?: boolean;\n  /** Sets `-parallel-testing-worker-count`. */\n  concurrentWorkers?: number;\n  /** Sets `-maximum-concurrent-test-simulator-destinations`. */\n  maxConcurrentSimulators?: number;\n  /** Explicitly disable concurrent destinations (`-disable-concurrent-destination-testing`). */\n  disableConcurrentTesting?: boolean;\n  // ── Sanitizers ───────────────────────────────────────────────────────────\n  addressSanitizer?: boolean;\n  threadSanitizer?: boolean;\n  undefinedBehaviorSanitizer?: boolean;\n}\n\nexport function buildTestCommand(options: TestCommandOptions): string[] {\n  const args: string[] = [];\n\n  if (options.clean) args.push(\"clean\");\n  args.push(\"test\");\n\n  if (options.workspace) args.push(\"-workspace\", options.workspace);\n  else if (options.project) args.push(\"-project\", options.project);\n\n  if (options.scheme) args.push(\"-scheme\", options.scheme);\n\n  // Destination\n  if (options.destination) {\n    args.push(\"-destination\", options.destination);\n  } else if (options.devices && options.devices.length > 0) {\n    for (const device of options.devices) {\n      args.push(\"-destination\", `platform=iOS Simulator,name=${device}`);\n    }\n  }\n\n  if (options.testPlan) args.push(\"-testPlan\", options.testPlan);\n  if (options.configuration) args.push(\"-configuration\", options.configuration);\n  if (options.derivedDataPath) args.push(\"-derivedDataPath\", options.derivedDataPath);\n  if (options.resultBundlePath) args.push(\"-resultBundlePath\", options.resultBundlePath);\n\n  if (options.codeCoverage) args.push(\"-enableCodeCoverage\", \"YES\");\n\n  if (options.onlyTesting) {\n    for (const test of options.onlyTesting) {\n      args.push(\"-only-testing:\" + test);\n    }\n  }\n  if (options.skipTesting) {\n    for (const test of options.skipTesting) {\n      args.push(\"-skip-testing:\" + test);\n    }\n  }\n\n  // Parallel testing flags (xcodebuild native, not xcargs)\n  if (options.parallelTesting !== undefined) {\n    args.push(\"-parallel-testing-enabled\", options.parallelTesting ? \"YES\" : \"NO\");\n  }\n  if (options.concurrentWorkers !== undefined) {\n    args.push(\"-parallel-testing-worker-count\", String(options.concurrentWorkers));\n  }\n  if (options.maxConcurrentSimulators !== undefined) {\n    args.push(\n      \"-maximum-concurrent-test-simulator-destinations\",\n      String(options.maxConcurrentSimulators)\n    );\n  }\n  if (options.disableConcurrentTesting) {\n    args.push(\"-disable-concurrent-destination-testing\");\n  }\n\n  // Runtime sanitizers\n  if (options.addressSanitizer !== undefined) {\n    args.push(\"-enableAddressSanitizer\", options.addressSanitizer ? \"YES\" : \"NO\");\n  }\n  if (options.threadSanitizer !== undefined) {\n    args.push(\"-enableThreadSanitizer\", options.threadSanitizer ? \"YES\" : \"NO\");\n  }\n  if (options.undefinedBehaviorSanitizer !== undefined) {\n    args.push(\n      \"-enableUndefinedBehaviorSanitizer\",\n      options.undefinedBehaviorSanitizer ? \"YES\" : \"NO\"\n    );\n  }\n\n  if (options.xcargs) args.push(...options.xcargs.split(/\\s+/));\n\n  return args;\n}\n","export interface FailedTest {\n  target: string;\n  suite: string;\n  testCase: string;\n  onlyTestingArg: string;\n}\n\nexport function parseFailedTests(output: string): FailedTest[] {\n  const results: FailedTest[] = [];\n  // Strip ANSI color codes\n  const clean = output.replace(/\\x1b\\[[0-9;]*m/g, \"\");\n\n  // Look for \"Failing tests:\" section\n  const failingMatch = clean.match(/Failing tests:\\s*\\n([\\s\\S]*?)(?:\\n\\n|\\n\\*\\*|$)/);\n  if (!failingMatch) return results;\n\n  const lines = failingMatch[1].split(\"\\n\");\n  for (const line of lines) {\n    const trimmed = line.trim();\n    if (!trimmed || trimmed.startsWith(\"*\")) continue;\n\n    // Format: -[TestSuite testMethod] or TestTarget.TestSuite/testMethod\n    const bracketMatch = trimmed.match(/-\\[(\\w+)\\s+(\\w+)\\]/);\n    if (bracketMatch) {\n      results.push({\n        target: \"\",\n        suite: bracketMatch[1],\n        testCase: bracketMatch[2],\n        onlyTestingArg: `${bracketMatch[1]}/${bracketMatch[2]}`,\n      });\n      continue;\n    }\n\n    const slashMatch = trimmed.match(/^(\\w+)\\.(\\w+)\\/(\\w+)$/);\n    if (slashMatch) {\n      results.push({\n        target: slashMatch[1],\n        suite: slashMatch[2],\n        testCase: slashMatch[3],\n        onlyTestingArg: `${slashMatch[1]}/${slashMatch[2]}/${slashMatch[3]}`,\n      });\n    }\n  }\n\n  return results;\n}\n","import { spawn } from \"node:child_process\";\nimport { join } from \"node:path\";\nimport { mkdirSync, existsSync } from \"node:fs\";\nimport { requireMacOS } from \"../xcode/platform.js\";\nimport { buildTestCommand } from \"./command.js\";\nimport { parseFailedTests } from \"./retry.js\";\n\nexport type { FailedTest } from \"./retry.js\";\nexport { buildTestCommand } from \"./command.js\";\nexport { parseFailedTests } from \"./retry.js\";\n\nexport interface ScanOptions {\n  scheme?: string;\n  workspace?: string;\n  project?: string;\n  devices?: string[];\n  testPlan?: string;\n  onlyTesting?: string[];\n  skipTesting?: string[];\n  configuration?: string;\n  outputDirectory?: string;\n  clean?: boolean;\n  codeCoverage?: boolean;\n  derivedDataPath?: string;\n  resultBundlePath?: string;\n  maxRetries?: number;\n  xcargs?: string;\n  onOutput?: (chunk: string) => void;\n}\n\nexport interface TestResult {\n  success: boolean;\n  totalTests: number;\n  failures: number;\n  skipped: number;\n  retries: number;\n  duration: number;\n  reportPaths: string[];\n  output: string;\n}\n\nexport async function runTests(options: ScanOptions): Promise<TestResult> {\n  requireMacOS(\"scan\");\n\n  const outputDir = options.outputDirectory ?? join(process.cwd(), \"test_output\");\n  mkdirSync(outputDir, { recursive: true });\n\n  const resultBundlePath = options.resultBundlePath ?? join(outputDir, \"result.xcresult\");\n  const maxRetries = options.maxRetries ?? 0;\n  let retryCount = 0;\n  let lastOutput = \"\";\n  let currentOnlyTesting = options.onlyTesting;\n\n  const startTime = Date.now();\n\n  for (let attempt = 0; attempt <= maxRetries; attempt++) {\n    const args = buildTestCommand({\n      ...options,\n      resultBundlePath: attempt === 0 ? resultBundlePath : `${resultBundlePath}-retry-${attempt}`,\n      onlyTesting: currentOnlyTesting,\n    });\n\n    const result = await runXcodebuild(args, options.onOutput);\n    lastOutput = result.output;\n\n    if (result.exitCode === 0) {\n      // Tests passed\n      break;\n    }\n\n    if (attempt < maxRetries) {\n      // Parse failures for retry\n      const failed = parseFailedTests(result.output);\n      if (failed.length === 0) break; // No parseable failures\n\n      currentOnlyTesting = failed.map((f) => f.onlyTestingArg);\n      retryCount++;\n    }\n  }\n\n  // Parse test counts from output\n  const counts = parseTestCounts(lastOutput);\n  const duration = (Date.now() - startTime) / 1000;\n\n  return {\n    success: counts.failures === 0,\n    totalTests: counts.total,\n    failures: counts.failures,\n    skipped: counts.skipped,\n    retries: retryCount,\n    duration,\n    reportPaths: existsSync(resultBundlePath) ? [resultBundlePath] : [],\n    output: lastOutput,\n  };\n}\n\nfunction runXcodebuild(\n  args: string[],\n  onOutput?: (chunk: string) => void\n): Promise<{ exitCode: number; output: string }> {\n  return new Promise((resolve) => {\n    const child = spawn(\"xcodebuild\", args, { stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n    let output = \"\";\n\n    child.stdout.on(\"data\", (data: Buffer) => {\n      const chunk = data.toString();\n      output += chunk;\n      onOutput?.(chunk);\n    });\n    child.stderr.on(\"data\", (data: Buffer) => {\n      const chunk = data.toString();\n      output += chunk;\n      onOutput?.(chunk);\n    });\n    child.on(\"close\", (code) => resolve({ exitCode: code ?? 1, output }));\n  });\n}\n\nfunction parseTestCounts(output: string): { total: number; failures: number; skipped: number } {\n  // Match: \"Executed N tests, with M failures (X unexpected) in Y (Z) seconds\"\n  const match = output.match(/Executed (\\d+) tests?, with (\\d+) failures?/);\n  if (match) {\n    return {\n      total: parseInt(match[1], 10),\n      failures: parseInt(match[2], 10),\n      skipped: 0,\n    };\n  }\n  return { total: 0, failures: 0, skipped: 0 };\n}\n"]}