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
41 changes: 41 additions & 0 deletions docs/standards/coding-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,46 @@ See [logging-examples.md](coding-standards/logging-examples.md) for patterns.
- Test project root namespaces should omit the `*.Tests` or `.Benchmarks` suffix
- Project internals should be exposed to the Arch, Unit and System Test Projects

**Test output:** All test classes must use appropriate output helpers for diagnostic logging:

| Test Type | Output Helper | Namespace |
| ----------- | ----------------------- | ------------------------- |
| xUnit tests | `ITestOutputHelper` | `Xunit.Abstractions` |
| Reqnroll | `IReqnrollOutputHelper` | `Reqnroll.Infrastructure` |

Inject via constructor and use `WriteLine` for diagnostic output visible in test runners:

```csharp
// xUnit unit test
public sealed class MyTests
{
private readonly ITestOutputHelper _output;

public MyTests(ITestOutputHelper output) => _output = output;

[Fact]
public void MyTest()
{
_output.WriteLine("Running test with value: {0}", value);
}
}

// Reqnroll step definitions
[Binding]
public sealed class MySteps
{
private readonly IReqnrollOutputHelper _output;

public MySteps(IReqnrollOutputHelper output) => _output = output;

[When("I do something")]
public void WhenIDoSomething()
{
_output.WriteLine("Executing step with context: {0}", context);
}
}
```

**Coverage target:** 80% line, 70% branch on changed code

See [testing-examples.md](coding-standards/testing-examples.md) for patterns and BDD guidance.
Expand Down Expand Up @@ -197,6 +237,7 @@ See [security-examples.md](coding-standards/security-examples.md) for examples.
- [ ] `CancellationToken` passed through
- [ ] Uses structured logging
- [ ] Has appropriate tests
- [ ] Tests use `ITestOutputHelper`/`IReqnrollOutputHelper`
- [ ] XML docs on public members
- [ ] No analyzer warnings
- [ ] No hardcoded secrets
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
global using AwesomeAssertions;
global using Reqnroll;
global using Reqnroll.Infrastructure;
global using Xunit;
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,52 @@ namespace McjCoderOrg.ClaudeAutoResume.E2ETests.StepDefinitions;
#pragma warning disable CA1822 // SpecFlow step definition methods cannot be static
public sealed class ApplicationStartupSteps : IDisposable
{
private readonly IReqnrollOutputHelper _output;
private Process? _process;
private ProcessResult? _result;

public ApplicationStartupSteps(IReqnrollOutputHelper output)
{
_output = output;
}

[Given("the executable exists")]
public void GivenTheExecutableExists()
{
var execPath = ProcessHelper.GetExecutablePath();
_output.WriteLine("Checking executable at: {0}", execPath);
Skip.IfNot(
ProcessHelper.ExecutableExists(),
$"Executable not found at {ProcessHelper.GetExecutablePath()}");
$"Executable not found at {execPath}");
_output.WriteLine("Executable found");
}

[Given("claude CLI is available")]
public void GivenClaudeCliIsAvailable()
{
_output.WriteLine("Checking Claude CLI availability");
var isAvailable = ProcessHelper.IsClaudeAvailable();
_output.WriteLine("Claude CLI available: {0}", isAvailable);
Skip.IfNot(
ProcessHelper.IsClaudeAvailable(),
isAvailable,
"Claude CLI is not available - skipping test");
}

[Given("claude CLI is not available")]
public void GivenClaudeCliIsNotAvailable()
{
_output.WriteLine("Checking Claude CLI is NOT available");
var isAvailable = ProcessHelper.IsClaudeAvailable();
_output.WriteLine("Claude CLI available: {0}", isAvailable);
Skip.If(
ProcessHelper.IsClaudeAvailable(),
isAvailable,
"Claude CLI is available - this test is for missing claude");
}

[When("I run the application with {string}")]
public async Task WhenIRunTheApplicationWith(string arguments)
{
_output.WriteLine("Running application with arguments: {0}", arguments);
_process = ProcessHelper.CreateProcess(arguments);
_process.Start();

Expand All @@ -46,6 +62,16 @@ public async Task WhenIRunTheApplicationWith(string arguments)
await _process.WaitForExitAsync().ConfigureAwait(false);

_result = new ProcessResult(_process.ExitCode, stdout, stderr);
_output.WriteLine("Exit code: {0}", _result.ExitCode);
if (!string.IsNullOrEmpty(_result.StandardOutput))
{
_output.WriteLine("Stdout: {0}", _result.StandardOutput);
}

if (!string.IsNullOrEmpty(_result.StandardError))
{
_output.WriteLine("Stderr: {0}", _result.StandardError);
}
}

[When("I run the application with no arguments")]
Expand All @@ -57,24 +83,28 @@ public async Task WhenIRunTheApplicationWithNoArguments()
[Then("the exit code should be {int}")]
public void ThenTheExitCodeShouldBe(int expectedExitCode)
{
_output.WriteLine("Verifying exit code: expected={0}, actual={1}", expectedExitCode, Result.ExitCode);
Result.ExitCode.Should().Be(expectedExitCode);
}

[Then("the output should contain {string}")]
public void ThenTheOutputShouldContain(string expected)
{
_output.WriteLine("Verifying stdout contains: {0}", expected);
Result.StandardOutput.Should().Contain(expected);
}

[Then("the error output should contain {string}")]
public void ThenTheErrorOutputShouldContain(string expected)
{
_output.WriteLine("Verifying stderr contains: {0}", expected);
Result.StandardError.Should().Contain(expected);
}

[Then("the combined output should contain {string}")]
public void ThenTheCombinedOutputShouldContain(string expected)
{
_output.WriteLine("Verifying combined output contains: {0}", expected);
Result.CombinedOutput.Should().Contain(expected);
}

Expand Down Expand Up @@ -250,18 +280,22 @@ public async Task WhenISendToTheApplication(string input)
[Given("bash is available")]
public void GivenBashIsAvailable()
{
var bashPath = ProcessHelper.GetBashPath();
_output.WriteLine("Checking bash availability: {0}", bashPath ?? "(not found)");
Skip.IfNot(
ProcessHelper.GetBashPath() is not null,
bashPath is not null,
"Bash not found - Git Bash is required on Windows");
}

[When("I run the application via shell with {string} piped after {int} seconds")]
public async Task WhenIRunTheApplicationViaShellWithPipedInput(string input, int delaySeconds)
{
_output.WriteLine("Running application via shell with input '{0}' piped after {1} seconds", input, delaySeconds);
// Use a generous timeout: delay + time for claude to start + time to process exit
var timeoutSeconds = delaySeconds + 60;
_result = await ProcessHelper.RunViaShellWithPipedInputAsync(input, delaySeconds, timeoutSeconds)
.ConfigureAwait(false);
_output.WriteLine("Shell execution completed with exit code: {0}", _result.ExitCode);
}

[Then("the application should exit within {int} seconds")]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
global using AwesomeAssertions;
global using Moq;
global using Reqnroll;
global using Reqnroll.Infrastructure;
global using Xunit;
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@ namespace McjCoderOrg.ClaudeAutoResume.StepDefinitions;
[Binding]
public sealed class AutoResumeSteps
{
private readonly IReqnrollOutputHelper _output;
private WrapperConfig _config = WrapperConfig.Default;
private string? _sentCommand;
private bool _bufferCleared;
private int _expectedWaitMinutes;

public AutoResumeSteps(IReqnrollOutputHelper output)
{
_output = output;
}

[Given("a rate limit has been detected")]
public void GivenARateLimitHasBeenDetected()
{
// State tracking for rate limit - simulation only
_expectedWaitMinutes = _config.WaitMinutes;
_output.WriteLine("Rate limit detected, wait time: {0} minutes", _expectedWaitMinutes);
}

[Given("the wait time is configured to {int} minutes")]
public void GivenTheWaitTimeIsConfiguredToMinutes(int minutes)
{
_output.WriteLine("Configuring wait time to {0} minutes", minutes);
_config = _config with { WaitMinutes = minutes };
_expectedWaitMinutes = minutes;
}
Expand Down Expand Up @@ -51,18 +59,21 @@ public void WhenResumingAfterRateLimit()
[Then("the continue command should be sent")]
public void ThenTheContinueCommandShouldBeSent()
{
_output.WriteLine("Verifying continue command was sent: {0}", _sentCommand ?? "(null)");
_sentCommand.Should().NotBeNull("the continue command should have been sent");
}

[Then("the output buffer should be cleared")]
public void ThenTheOutputBufferShouldBeCleared()
{
_output.WriteLine("Verifying output buffer cleared: {0}", _bufferCleared);
_bufferCleared.Should().BeTrue("the output buffer should be cleared after rate limit");
}

[Then("the wrapper should wait for {int} minutes")]
public void ThenTheWrapperShouldWaitForMinutes(int minutes)
{
_output.WriteLine("Verifying wait time: expected={0}, actual={1}", minutes, _expectedWaitMinutes);
_expectedWaitMinutes.Should().Be(minutes);
_config.WaitMinutes.Should().Be(minutes);
}
Expand All @@ -71,12 +82,14 @@ public void ThenTheWrapperShouldWaitForMinutes(int minutes)
public void ThenStringShouldBeSentToThePty(string expected)
{
var unescaped = expected.Replace("\\n", "\n", StringComparison.Ordinal);
_output.WriteLine("Verifying PTY command: expected='{0}', actual='{1}'", unescaped, _sentCommand);
_sentCommand.Should().Be(unescaped);
}

[Then("a newline should be sent to the PTY")]
public void ThenANewlineShouldBeSentToThePty()
{
_output.WriteLine("Verifying newline sent to PTY");
_sentCommand.Should().Be("\n");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@ namespace McjCoderOrg.ClaudeAutoResume.StepDefinitions;
[Binding]
public sealed class HeadlessModeSteps
{
private readonly IReqnrollOutputHelper _output;
private WrapperConfig _config = WrapperConfig.Default;
private string _bufferContent = string.Empty;
private bool _promptDetected;
private string? _sentResponse;
private double _secondsSinceLastOutput;

public HeadlessModeSteps(IReqnrollOutputHelper output)
{
_output = output;
}

[Given("headless mode is enabled")]
public void GivenHeadlessModeIsEnabled()
{
_output.WriteLine("Enabling headless mode");
_config = _config with { Headless = true };
}

[Given("dangerous permissions are enabled")]
public void GivenDangerousPermissionsAreEnabled()
{
_output.WriteLine("Enabling dangerous permissions");
_config = _config with { DangerouslySkipPermissions = true };
}

Expand All @@ -44,6 +52,9 @@ public void GivenTheOutputBufferContainsForHeadless(string content)
[When("the prompt check runs")]
public void WhenThePromptCheckRuns()
{
_output.WriteLine("Running prompt check, buffer: '{0}', timeout: {1}s, elapsed: {2}s",
_bufferContent, _config.PromptTimeoutSeconds, _secondsSinceLastOutput);

// Simulate prompt detection logic from ClaudeMonitor
var matchFound = _config.PromptPatterns.Any(pattern =>
_bufferContent.Contains(pattern, StringComparison.OrdinalIgnoreCase));
Expand All @@ -52,29 +63,34 @@ public void WhenThePromptCheckRuns()
{
_promptDetected = true;
_sentResponse = _config.DefaultPromptResponse;
_output.WriteLine("Prompt detected, sending response: '{0}'", _sentResponse);
}
else
{
_promptDetected = false;
_output.WriteLine("No prompt detected (matchFound={0})", matchFound);
}
}

[Then("a prompt should be detected")]
public void ThenAPromptShouldBeDetected()
{
_output.WriteLine("Verifying prompt detected: {0}", _promptDetected);
_promptDetected.Should().BeTrue("a prompt pattern should have been matched");
}

[Then("{string} should be sent as response")]
public void ThenStringShouldBeSentAsResponse(string expected)
{
var unescaped = expected.Replace("\\n", "\n", StringComparison.Ordinal);
_output.WriteLine("Verifying response: expected='{0}', actual='{1}'", unescaped, _sentResponse);
_sentResponse.Should().Be(unescaped);
}

[Then("no prompt response should be sent")]
public void ThenNoPromptResponseShouldBeSent()
{
_output.WriteLine("Verifying no prompt response sent");
_promptDetected.Should().BeFalse("no prompt should be detected during active output");
_sentResponse.Should().BeNull();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,29 @@ namespace McjCoderOrg.ClaudeAutoResume.StepDefinitions;
[Binding]
public sealed class RateLimitDetectionSteps
{
private readonly IReqnrollOutputHelper _output;
private WrapperConfig _config = WrapperConfig.Default;
private string _bufferContent = string.Empty;
private bool _rateLimitDetected;
private bool _recentRateLimitDetected;
private bool _cooldownActive;

public RateLimitDetectionSteps(IReqnrollOutputHelper output)
{
_output = output;
}

[Given("the default wrapper configuration")]
public void GivenTheDefaultWrapperConfiguration()
{
_output.WriteLine("Using default wrapper configuration");
_config = WrapperConfig.Default;
}

[Given("the output buffer contains {string}")]
public void GivenTheOutputBufferContains(string content)
{
_output.WriteLine("Setting buffer content: '{0}'", content);
_bufferContent = content;
}

Expand Down Expand Up @@ -53,9 +61,12 @@ public void GivenTheCooldownPeriodHasNotElapsed()
[When("the rate limit check runs")]
public void WhenTheRateLimitCheckRuns()
{
_output.WriteLine("Running rate limit check on buffer: '{0}'", _bufferContent);

// Simulate rate limit detection logic
if (_cooldownActive && _recentRateLimitDetected)
{
_output.WriteLine("Skipping detection due to cooldown");
_rateLimitDetected = false;
return;
}
Expand Down Expand Up @@ -86,17 +97,20 @@ public void WhenTheRateLimitCheckRuns()
}

_rateLimitDetected = matchedPattern != null;
_output.WriteLine("Rate limit detected: {0}, matched pattern: {1}", _rateLimitDetected, matchedPattern ?? "(none)");
}

[Then("a rate limit should be detected")]
public void ThenARateLimitShouldBeDetected()
{
_output.WriteLine("Verifying rate limit detected: {0}", _rateLimitDetected);
_rateLimitDetected.Should().BeTrue("a rate limit pattern should have been matched");
}

[Then("no rate limit should be detected")]
public void ThenNoRateLimitShouldBeDetected()
{
_output.WriteLine("Verifying no rate limit detected: {0}", _rateLimitDetected);
_rateLimitDetected.Should().BeFalse("no rate limit pattern should have been matched");
}
}
Loading