Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions src/McjCoderOrg.ClaudeAutoResume/ClaudeMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ internal sealed class ClaudeMonitor : IClaudeMonitor
private bool _disposed;
private bool _potentialPromptDetected;

/// <summary>
/// Gets the current output buffer contents for testing.
/// </summary>
internal string OutputBufferContents
{
get
{
lock (_bufferLock)
{
return _outputBuffer.ToString();
}
}
}

/// <summary>
/// Initializes a new instance of the <see cref="ClaudeMonitor"/> class.
/// </summary>
Expand Down Expand Up @@ -333,7 +347,7 @@ private async Task HandleHangingPromptAsync(CancellationToken ct)
await _pty.WriterStream.FlushAsync(ct).ConfigureAwait(false);
}

private static string EscapeForDisplay(string s)
internal static string EscapeForDisplay(string s)
{
return s.Replace("\n", "\\n", StringComparison.Ordinal)
.Replace("\r", "\\r", StringComparison.Ordinal);
Expand Down Expand Up @@ -408,7 +422,7 @@ private async Task ForwardInteractiveInputAsync(Stream stream, CancellationToken
}
}

private static byte[] ConvertKeyToBytes(ConsoleKeyInfo k)
internal static byte[] ConvertKeyToBytes(ConsoleKeyInfo k)
{
return k.Key switch
{
Expand Down Expand Up @@ -452,7 +466,7 @@ private async Task MonitorWindowSizeAsync(CancellationToken ct)
}
}

private void AppendToBuffer(string text)
internal void AppendToBuffer(string text)
{
lock (_bufferLock)
{
Expand Down Expand Up @@ -538,7 +552,7 @@ private async Task SendContinueCommandAsync(CancellationToken ct)
await _pty.WriterStream.FlushAsync(ct).ConfigureAwait(false);
}

private string? FindClaudeInPath()
internal string? FindClaudeInPath()
{
var pathVar = _environment.GetEnvironmentVariable("PATH") ?? string.Empty;
var separator = _environment.IsWindows ? ';' : ':';
Expand Down Expand Up @@ -590,7 +604,7 @@ private async Task SendContinueCommandAsync(CancellationToken ct)
return null;
}

private Dictionary<string, string> GetEnvironment()
internal Dictionary<string, string> GetEnvironment()
{
var env = new Dictionary<string, string>(_environment.GetEnvironmentVariables(), StringComparer.Ordinal);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,26 @@ Feature: Rate Limit Detection
| too many requests, please wait |
| rate limit exceeded |
| quota exceeded, limit reached |

@unit
Scenario: Rate limit pattern split across buffer chunks
Given the output buffer contains "Your usage li"
And additional output arrives "mit has been reached"
When the rate limit check runs
Then a rate limit should be detected

@unit
Scenario: Buffer rotation preserves recent rate limit message
Given the output buffer is at capacity
And new output contains "usage limit reached"
When the buffer rotates old content
And the rate limit check runs
Then the rate limit message is preserved
And a rate limit should be detected

@unit
Scenario: Partial rate limit pattern not detected
Given the output buffer contains "limit"
But does not contain "reached" or "reset"
When the rate limit check runs
Then no rate limit should be detected

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,64 @@ public void GivenTheCooldownPeriodHasNotElapsed()
_cooldownActive = true;
}

[Given("additional output arrives {string}")]
public void GivenAdditionalOutputArrives(string content)
{
_output.WriteLine("Appending to buffer: '{0}'", content);
_bufferContent += content;
}

[Given("the output buffer is at capacity")]
public void GivenTheOutputBufferIsAtCapacity()
{
// Fill buffer with placeholder content (simulating capacity)
_output.WriteLine("Setting buffer to capacity with placeholder content");
_bufferContent = new string('X', 1000);
}

[Given("new output contains {string}")]
public void GivenNewOutputContains(string content)
{
_output.WriteLine("Adding new output: '{0}'", content);
_bufferContent += content;
}

[Given("does not contain {string} or {string}")]
public void GivenDoesNotContainEitherKeyword(string keyword1, string keyword2)
{
// Validation step - ensure buffer does not contain either keyword
var containsKeyword1 = _bufferContent.Contains(keyword1, StringComparison.OrdinalIgnoreCase);
var containsKeyword2 = _bufferContent.Contains(keyword2, StringComparison.OrdinalIgnoreCase);
(containsKeyword1 || containsKeyword2).Should().BeFalse(
"buffer should not contain '{0}' or '{1}'", keyword1, keyword2);
}

[When("the buffer rotates old content")]
public void WhenTheBufferRotatesOldContent()
{
// Simulate buffer rotation by keeping only recent content
_output.WriteLine("Simulating buffer rotation");
const int recentContentLength = 500;
if (_bufferContent.Length > recentContentLength)
{
_bufferContent = _bufferContent[^recentContentLength..];
}

_output.WriteLine("Buffer after rotation: '{0}'", _bufferContent);
}

[Then("the rate limit message is preserved")]
public void ThenTheRateLimitMessageIsPreserved()
{
_output.WriteLine("Verifying rate limit message preserved in buffer");
var hasRateLimitIndicator =
_bufferContent.Contains("limit", StringComparison.OrdinalIgnoreCase) &&
(_bufferContent.Contains("reached", StringComparison.OrdinalIgnoreCase) ||
_bufferContent.Contains("reset", StringComparison.OrdinalIgnoreCase) ||
_bufferContent.Contains("exceeded", StringComparison.OrdinalIgnoreCase));
hasRateLimitIndicator.Should().BeTrue("rate limit message should be preserved after rotation");
}

[When("the rate limit check runs")]
public void WhenTheRateLimitCheckRuns()
{
Expand Down
Loading