Skip to content

Commit 299cb18

Browse files
committed
added redirect standart input + kill process if something goes wrong so it wont loop test execution
1 parent cf51a3a commit 299cb18

File tree

4 files changed

+44
-6
lines changed

4 files changed

+44
-6
lines changed

ClaudeCodeSharpSDK.Tests/Integration/ClaudeCliSmokeTests.cs

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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,

ClaudeCodeSharpSDK.Tests/Integration/RealClaudeIntegrationTests.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace ManagedCode.ClaudeCodeSharpSDK.Tests.Integration;
88
public class RealClaudeIntegrationTests
99
{
1010
private const string ReplyWithOkOnlyPrompt = "Reply with OK only.";
11+
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
1112

1213
[Test]
1314
public async Task RealClaude_RunAsync_WhenAuthenticated_ReturnsResponse()
@@ -23,7 +24,11 @@ public async Task RealClaude_RunAsync_WhenAuthenticated_ReturnsResponse()
2324
NoSessionPersistence = true,
2425
});
2526

26-
var result = await thread.RunAsync(ReplyWithOkOnlyPrompt);
27+
using var timeoutCts = new CancellationTokenSource(TestTimeout);
28+
var result = await thread.RunAsync(ReplyWithOkOnlyPrompt, new TurnOptions
29+
{
30+
CancellationToken = timeoutCts.Token,
31+
});
2732

2833
await Assert.That(string.IsNullOrWhiteSpace(result.FinalResponse)).IsFalse();
2934
}

ClaudeCodeSharpSDK.Tests/Shared/RealClaudeTestSupport.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ private static bool IsAuthenticated(string executablePath)
7575
{
7676
var startInfo = new ProcessStartInfo(executablePath)
7777
{
78+
RedirectStandardInput = true,
7879
RedirectStandardOutput = true,
7980
RedirectStandardError = true,
8081
UseShellExecute = false,
@@ -89,6 +90,7 @@ private static bool IsAuthenticated(string executablePath)
8990
return false;
9091
}
9192

93+
process.StandardInput.Close();
9294
var standardOutput = process.StandardOutput.ReadToEnd();
9395
var standardError = process.StandardError.ReadToEnd();
9496
process.WaitForExit();

ClaudeCodeSharpSDK/Internal/ClaudeCliMetadataReader.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,7 @@ private static string ReadInstalledVersion(string executablePath)
320320
{
321321
var startInfo = new ProcessStartInfo(executablePath)
322322
{
323+
RedirectStandardInput = true,
323324
RedirectStandardOutput = true,
324325
RedirectStandardError = true,
325326
UseShellExecute = false,
@@ -331,6 +332,7 @@ private static string ReadInstalledVersion(string executablePath)
331332
?? throw new InvalidOperationException(
332333
string.Concat(StartExecutableFailedMessagePrefix, Space, MessageQuote, executablePath, MessageQuote, MessageSuffix));
333334

335+
process.StandardInput.Close();
334336
var standardOutput = process.StandardOutput.ReadToEnd();
335337
var standardError = process.StandardError.ReadToEnd();
336338
process.WaitForExit();
@@ -359,6 +361,7 @@ private static (string? LatestVersion, string? ErrorMessage) ProbeLatestPublishe
359361
var gitExecutable = OperatingSystem.IsWindows() ? string.Concat(GitExecutableName, ExecutableExtension) : GitExecutableName;
360362
var startInfo = new ProcessStartInfo(gitExecutable)
361363
{
364+
RedirectStandardInput = true,
362365
RedirectStandardOutput = true,
363366
RedirectStandardError = true,
364367
UseShellExecute = false,
@@ -376,6 +379,7 @@ private static (string? LatestVersion, string? ErrorMessage) ProbeLatestPublishe
376379
return (null, StartGitProcessFailedMessage);
377380
}
378381

382+
process.StandardInput.Close();
379383
var standardOutput = process.StandardOutput.ReadToEnd();
380384
var standardError = process.StandardError.ReadToEnd();
381385
process.WaitForExit();

0 commit comments

Comments
 (0)