@@ -37,10 +37,12 @@ public class ClaudeCliSmokeTests
3737 private const string FailedToResolveExecutablePathMessage = "Failed to resolve Claude Code CLI path." ;
3838 private const string CouldNotLocateRepositoryRootMessage = "Could not locate repository root from test execution directory." ;
3939 private const string StartProcessFailedMessagePrefix = "Failed to start Claude Code CLI at" ;
40+ private const string ClaudeCodeNestingEnvironmentVariable = "CLAUDECODE" ;
4041 private const string Space = " " ;
4142 private const string MessageQuote = "'" ;
4243 private const string MessageSuffix = "." ;
4344 private static readonly string [ ] StandardLineSeparators = [ Environment . NewLine , NewLine , CarriageReturn ] ;
45+ private static readonly TimeSpan TestTimeout = TimeSpan . FromSeconds ( 30 ) ;
4446
4547 [ Test ]
4648 public async Task ClaudeCli_Smoke_FindExecutablePath_ResolvesExistingBinary ( )
@@ -52,7 +54,8 @@ public async Task ClaudeCli_Smoke_FindExecutablePath_ResolvesExistingBinary()
5254 [ Test ]
5355 public async Task ClaudeCli_Smoke_VersionCommand_ReturnsClaudeCodeVersion ( )
5456 {
55- var result = await RunClaudeAsync ( ResolveExecutablePath ( ) , null , VersionFlag ) ;
57+ using var timeoutCts = new CancellationTokenSource ( TestTimeout ) ;
58+ var result = await RunClaudeAsync ( ResolveExecutablePath ( ) , null , timeoutCts . Token , VersionFlag ) ;
5659
5760 await Assert . That ( result . ExitCode ) . IsEqualTo ( 0 ) ;
5861 await Assert . That ( string . Concat ( result . StandardOutput , result . StandardError ) )
@@ -62,7 +65,8 @@ await Assert.That(string.Concat(result.StandardOutput, result.StandardError))
6265 [ Test ]
6366 public async Task ClaudeCli_Smoke_HelpCommand_DescribesStreamJsonOutput ( )
6467 {
65- var result = await RunClaudeAsync ( ResolveExecutablePath ( ) , null , HelpFlag ) ;
68+ using var timeoutCts = new CancellationTokenSource ( TestTimeout ) ;
69+ var result = await RunClaudeAsync ( ResolveExecutablePath ( ) , null , timeoutCts . Token , HelpFlag ) ;
6670
6771 await Assert . That ( result . ExitCode ) . IsEqualTo ( 0 ) ;
6872 await Assert . That ( string . Concat ( result . StandardOutput , result . StandardError ) )
@@ -73,12 +77,14 @@ await Assert.That(string.Concat(result.StandardOutput, result.StandardError))
7377 public async Task ClaudeCli_Smoke_PrintModeWithoutAuth_EmitsInitAndLoginGuidance ( )
7478 {
7579 var sandboxDirectory = CreateSandboxDirectory ( ) ;
80+ using var timeoutCts = new CancellationTokenSource ( TestTimeout ) ;
7681
7782 try
7883 {
7984 var result = await RunClaudeAsync (
8085 ResolveExecutablePath ( ) ,
8186 CreateUnauthenticatedEnvironmentOverrides ( sandboxDirectory ) ,
87+ timeoutCts . Token ,
8288 PrintFlag ,
8389 OutputFormatFlag ,
8490 StreamJsonFormat ,
@@ -190,10 +196,12 @@ private static string ResolveRepositoryRootPath()
190196 private static async Task < ClaudeProcessResult > RunClaudeAsync (
191197 string executablePath ,
192198 IReadOnlyDictionary < string , string > ? environmentOverrides ,
199+ CancellationToken cancellationToken = default ,
193200 params string [ ] arguments )
194201 {
195202 var startInfo = new ProcessStartInfo ( executablePath )
196203 {
204+ RedirectStandardInput = true ,
197205 RedirectStandardOutput = true ,
198206 RedirectStandardError = true ,
199207 UseShellExecute = false ,
@@ -205,6 +213,8 @@ private static async Task<ClaudeProcessResult> RunClaudeAsync(
205213 startInfo . ArgumentList . Add ( argument ) ;
206214 }
207215
216+ startInfo . Environment . Remove ( ClaudeCodeNestingEnvironmentVariable ) ;
217+
208218 if ( environmentOverrides is not null )
209219 {
210220 foreach ( var ( key , value ) in environmentOverrides )
@@ -226,10 +236,27 @@ private static async Task<ClaudeProcessResult> RunClaudeAsync(
226236 MessageSuffix ) ) ;
227237 }
228238
229- var standardOutputTask = process . StandardOutput . ReadToEndAsync ( ) ;
230- var standardErrorTask = process . StandardError . ReadToEndAsync ( ) ;
239+ process . StandardInput . Close ( ) ;
240+
241+ using var registration = cancellationToken . Register ( ( ) =>
242+ {
243+ try
244+ {
245+ if ( ! process . HasExited )
246+ {
247+ process . Kill ( entireProcessTree : true ) ;
248+ }
249+ }
250+ catch ( InvalidOperationException )
251+ {
252+ // Process already exited between check and kill — safe to ignore.
253+ }
254+ } ) ;
255+
256+ var standardOutputTask = process . StandardOutput . ReadToEndAsync ( cancellationToken ) ;
257+ var standardErrorTask = process . StandardError . ReadToEndAsync ( cancellationToken ) ;
231258
232- await process . WaitForExitAsync ( ) ;
259+ await process . WaitForExitAsync ( cancellationToken ) ;
233260
234261 return new ClaudeProcessResult (
235262 process . ExitCode ,
0 commit comments