11using System . Diagnostics ;
2+ using System . Runtime . ExceptionServices ;
23using System . Text . Json ;
34using ManagedCode . ClaudeCodeSharpSDK . Client ;
45using ManagedCode . ClaudeCodeSharpSDK . Configuration ;
@@ -38,6 +39,7 @@ public class ClaudeCliSmokeTests
3839 private const string CarriageReturn = "\r " ;
3940 private const string CouldNotLocateRepositoryRootMessage = "Could not locate repository root from test execution directory." ;
4041 private const string StartProcessFailedMessagePrefix = "Failed to start Claude Code CLI at" ;
42+ private const string SmokeCleanupFailedMessage = "Claude CLI smoke process cleanup could not be fully confirmed." ;
4143 private const string ClaudeCodeNestingEnvironmentVariable = "CLAUDECODE" ;
4244 private const string Space = " " ;
4345 private const string MessageQuote = "'" ;
@@ -220,6 +222,8 @@ private static async Task<ClaudeProcessResult> RunClaudeAsync(
220222
221223 Task < string > ? standardOutputTask = null ;
222224 Task < string > ? standardErrorTask = null ;
225+ ClaudeProcessResult ? result = null ;
226+ Exception ? invocationFailure = null ;
223227 try
224228 {
225229 process . StandardInput . Close ( ) ;
@@ -228,34 +232,88 @@ private static async Task<ClaudeProcessResult> RunClaudeAsync(
228232 await process . WaitForExitAsync ( cancellationToken ) ;
229233 var output = await Task . WhenAll ( standardOutputTask , standardErrorTask )
230234 . WaitAsync ( TestCleanupTimeout , cancellationToken ) ;
231- return new ClaudeProcessResult ( process . ExitCode , output [ 0 ] , output [ 1 ] ) ;
235+ result = new ClaudeProcessResult ( process . ExitCode , output [ 0 ] , output [ 1 ] ) ;
232236 }
233- finally
237+ catch ( Exception exception )
234238 {
235- if ( ! process . HasExited )
239+ invocationFailure = exception ;
240+ }
241+
242+ var cleanupFailures = new List < Exception > ( ) ;
243+ if ( ! process . HasExited )
244+ {
245+ try
246+ {
247+ process . Kill ( entireProcessTree : true ) ;
248+ }
249+ catch ( InvalidOperationException ) when ( process . HasExited )
250+ {
251+ // The CLI exited between the state check and the kill request.
252+ }
253+ catch ( Exception exception )
236254 {
237- try
238- {
239- process . Kill ( entireProcessTree : true ) ;
240- }
241- catch ( InvalidOperationException ) when ( process . HasExited )
242- {
243- // The CLI exited between the state check and the kill request.
244- }
255+ cleanupFailures . Add ( exception ) ;
245256 }
257+ }
246258
247- if ( ! process . HasExited )
259+ if ( ! process . HasExited )
260+ {
261+ try
248262 {
249263 await process . WaitForExitAsync ( ) . WaitAsync ( TestCleanupTimeout ) ;
250264 }
265+ catch ( Exception exception )
266+ {
267+ cleanupFailures . Add ( exception ) ;
268+ }
269+ }
251270
271+ try
272+ {
252273 process . StandardOutput . Dispose ( ) ;
274+ }
275+ catch ( Exception exception )
276+ {
277+ cleanupFailures . Add ( exception ) ;
278+ }
279+
280+ try
281+ {
253282 process . StandardError . Dispose ( ) ;
254- if ( standardOutputTask is not null && standardErrorTask is not null )
283+ }
284+ catch ( Exception exception )
285+ {
286+ cleanupFailures . Add ( exception ) ;
287+ }
288+
289+ if ( standardOutputTask is not null && standardErrorTask is not null )
290+ {
291+ try
255292 {
256293 await ObserveReadersAsync ( standardOutputTask , standardErrorTask , cancellationToken ) ;
257294 }
295+ catch ( Exception exception )
296+ {
297+ cleanupFailures . Add ( exception ) ;
298+ }
258299 }
300+
301+ if ( cleanupFailures . Count != 0 )
302+ {
303+ if ( invocationFailure is not null )
304+ {
305+ cleanupFailures . Insert ( 0 , invocationFailure ) ;
306+ }
307+
308+ throw new AggregateException ( SmokeCleanupFailedMessage , cleanupFailures ) ;
309+ }
310+
311+ if ( invocationFailure is not null )
312+ {
313+ ExceptionDispatchInfo . Capture ( invocationFailure ) . Throw ( ) ;
314+ }
315+
316+ return result ! ;
259317 }
260318
261319 private static async Task ObserveReadersAsync (
0 commit comments