diff --git a/docs/standards/coding-standards.md b/docs/standards/coding-standards.md index 737771c..bd15fa3 100644 --- a/docs/standards/coding-standards.md +++ b/docs/standards/coding-standards.md @@ -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. @@ -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 diff --git a/tests/McjCoderOrg.ClaudeAutoResume.E2ETests/GlobalUsings.cs b/tests/McjCoderOrg.ClaudeAutoResume.E2ETests/GlobalUsings.cs index 5d8f9e3..30858d2 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.E2ETests/GlobalUsings.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.E2ETests/GlobalUsings.cs @@ -1,3 +1,4 @@ global using AwesomeAssertions; global using Reqnroll; +global using Reqnroll.Infrastructure; global using Xunit; diff --git a/tests/McjCoderOrg.ClaudeAutoResume.E2ETests/StepDefinitions/ApplicationStartupSteps.cs b/tests/McjCoderOrg.ClaudeAutoResume.E2ETests/StepDefinitions/ApplicationStartupSteps.cs index 2259359..8eb915a 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.E2ETests/StepDefinitions/ApplicationStartupSteps.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.E2ETests/StepDefinitions/ApplicationStartupSteps.cs @@ -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(); @@ -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")] @@ -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); } @@ -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")] diff --git a/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/GlobalUsings.cs b/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/GlobalUsings.cs index cdfdd01..c591836 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/GlobalUsings.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/GlobalUsings.cs @@ -1,4 +1,5 @@ global using AwesomeAssertions; global using Moq; global using Reqnroll; +global using Reqnroll.Infrastructure; global using Xunit; diff --git a/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/AutoResumeSteps.cs b/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/AutoResumeSteps.cs index 93c0c2f..bec6667 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/AutoResumeSteps.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/AutoResumeSteps.cs @@ -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; } @@ -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); } @@ -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"); } diff --git a/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/HeadlessModeSteps.cs b/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/HeadlessModeSteps.cs index dc030d4..73d0183 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/HeadlessModeSteps.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/HeadlessModeSteps.cs @@ -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 }; } @@ -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)); @@ -52,16 +63,19 @@ 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"); } @@ -69,12 +83,14 @@ public void ThenAPromptShouldBeDetected() 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(); } diff --git a/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/RateLimitDetectionSteps.cs b/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/RateLimitDetectionSteps.cs index e0feebf..87ac2c6 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/RateLimitDetectionSteps.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.SystemTests/StepDefinitions/RateLimitDetectionSteps.cs @@ -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; } @@ -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; } @@ -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"); } } diff --git a/tests/McjCoderOrg.ClaudeAutoResume.Tests/ClaudeMonitorTests.cs b/tests/McjCoderOrg.ClaudeAutoResume.Tests/ClaudeMonitorTests.cs index 019bda2..8b291e0 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.Tests/ClaudeMonitorTests.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.Tests/ClaudeMonitorTests.cs @@ -4,11 +4,14 @@ namespace McjCoderOrg.ClaudeAutoResume; public sealed class ClaudeMonitorTests : IDisposable { + private readonly ITestOutputHelper _output; private readonly ClaudeMonitor _monitor; - public ClaudeMonitorTests() + public ClaudeMonitorTests(ITestOutputHelper output) { + _output = output; _monitor = new ClaudeMonitor(WrapperConfig.Default); + _output.WriteLine("ClaudeMonitorTests initialized with default config"); } public void Dispose() @@ -79,6 +82,8 @@ public void BuildCommandLine_WithAllOptions_IncludesAllInCorrectOrder() var result = monitor.BuildCommandLine(["--extra"]); + _output.WriteLine("Built command line: [{0}]", string.Join(", ", result)); + // Verify order: dangerous, continue, prompt, additional args result.Should().HaveCount(5); result[0].Should().Be("--dangerously-skip-permissions"); diff --git a/tests/McjCoderOrg.ClaudeAutoResume.Tests/GlobalUsings.cs b/tests/McjCoderOrg.ClaudeAutoResume.Tests/GlobalUsings.cs index 4a2fa70..496f771 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.Tests/GlobalUsings.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.Tests/GlobalUsings.cs @@ -1,3 +1,4 @@ global using AwesomeAssertions; global using Moq; global using Xunit; +global using Xunit.Abstractions; diff --git a/tests/McjCoderOrg.ClaudeAutoResume.Tests/ProgramTests.cs b/tests/McjCoderOrg.ClaudeAutoResume.Tests/ProgramTests.cs index 7157b80..94486c7 100644 --- a/tests/McjCoderOrg.ClaudeAutoResume.Tests/ProgramTests.cs +++ b/tests/McjCoderOrg.ClaudeAutoResume.Tests/ProgramTests.cs @@ -2,51 +2,70 @@ namespace McjCoderOrg.ClaudeAutoResume; public sealed class ProgramTests { + private readonly ITestOutputHelper _output; + + public ProgramTests(ITestOutputHelper output) + { + _output = output; + } + [Fact] public async Task Main_WithVersionFlag_ShouldReturnSuccessAsync() { + _output.WriteLine("Testing --version flag"); var result = await Program.Main(["--version"]); + _output.WriteLine("Exit code: {0}", result); result.Should().Be(ExitCodes.Success); } [Fact] public async Task Main_WithHelpFlag_ShouldReturnSuccessAsync() { + _output.WriteLine("Testing --help flag"); var result = await Program.Main(["--help"]); + _output.WriteLine("Exit code: {0}", result); result.Should().Be(ExitCodes.Success); } [Fact] public async Task Main_WithDiagnoseFlag_ShouldReturnSuccessAsync() { + _output.WriteLine("Testing --diagnose flag"); var result = await Program.Main(["--diagnose"]); + _output.WriteLine("Exit code: {0}", result); result.Should().Be(ExitCodes.Success); } [Fact] public async Task Main_WithHeadlessWithoutDangerous_ShouldReturnInvalidArgumentsAsync() { + _output.WriteLine("Testing --headless without --dangerously-skip-permissions"); var result = await Program.Main(["--headless"]); + _output.WriteLine("Exit code: {0} (expected: {1})", result, ExitCodes.InvalidArguments); result.Should().Be(ExitCodes.InvalidArguments); } [Fact] public async Task Main_WithPromptWithoutValue_ShouldReturnInvalidArgumentsAsync() { + _output.WriteLine("Testing --prompt without value"); var result = await Program.Main(["--prompt"]); + _output.WriteLine("Exit code: {0} (expected: {1})", result, ExitCodes.InvalidArguments); result.Should().Be(ExitCodes.InvalidArguments); } [Fact] public async Task Main_WithWaitWithoutValue_ShouldReturnInvalidArgumentsAsync() { + _output.WriteLine("Testing --wait without value"); var result = await Program.Main(["--wait"]); + _output.WriteLine("Exit code: {0} (expected: {1})", result, ExitCodes.InvalidArguments); result.Should().Be(ExitCodes.InvalidArguments); } } diff --git a/tests/McjCoderOrg.HookTests/GlobalUsings.cs b/tests/McjCoderOrg.HookTests/GlobalUsings.cs index 5d8f9e3..30858d2 100644 --- a/tests/McjCoderOrg.HookTests/GlobalUsings.cs +++ b/tests/McjCoderOrg.HookTests/GlobalUsings.cs @@ -1,3 +1,4 @@ global using AwesomeAssertions; global using Reqnroll; +global using Reqnroll.Infrastructure; global using Xunit; diff --git a/tests/McjCoderOrg.HookTests/StepDefinitions/CommitMsgHookSteps.cs b/tests/McjCoderOrg.HookTests/StepDefinitions/CommitMsgHookSteps.cs index 63cac7f..cde0d16 100644 --- a/tests/McjCoderOrg.HookTests/StepDefinitions/CommitMsgHookSteps.cs +++ b/tests/McjCoderOrg.HookTests/StepDefinitions/CommitMsgHookSteps.cs @@ -7,10 +7,12 @@ namespace McjCoderOrg.HookTests.StepDefinitions; public sealed class CommitMsgHookSteps { private readonly ScenarioContext _scenarioContext; + private readonly IReqnrollOutputHelper _output; - public CommitMsgHookSteps(ScenarioContext scenarioContext) + public CommitMsgHookSteps(ScenarioContext scenarioContext, IReqnrollOutputHelper output) { _scenarioContext = scenarioContext; + _output = output; } private GitRepositoryFixture Fixture => (GitRepositoryFixture)_scenarioContext["Fixture"]; @@ -20,6 +22,7 @@ public CommitMsgHookSteps(ScenarioContext scenarioContext) [When("I create a commit message {string}")] public async Task WhenICreateACommitMessage(string message) { + _output.WriteLine("Creating commit message: {0}", message); await Fixture.CreateCommitMessageFileAsync(message).ConfigureAwait(false); } @@ -27,16 +30,24 @@ public async Task WhenICreateACommitMessage(string message) public async Task WhenICreateACommitMessageWithBody(string header, string body) { var fullMessage = $"{header}\n\n{body}"; + _output.WriteLine("Creating commit message with body: {0}", fullMessage); await Fixture.CreateCommitMessageFileAsync(fullMessage).ConfigureAwait(false); } [When("I run the commit-msg hook")] public async Task WhenIRunTheCommitMsgHook() { + _output.WriteLine("Running commit-msg hook"); ToolAvailability.SkipIfNodeMissing(); var commitMsgPath = Fixture.GetCommitMessageFilePath(); var result = await HookRunner.RunHookAsync("commit-msg", commitMsgPath).ConfigureAwait(false); + _output.WriteLine("Commit-msg exit code: {0}", result.ExitCode); + if (!string.IsNullOrEmpty(result.CombinedOutput)) + { + _output.WriteLine("Commit-msg output: {0}", result.CombinedOutput); + } + CommonSteps.SetLastResult(result); } diff --git a/tests/McjCoderOrg.HookTests/StepDefinitions/CommonHookSteps.cs b/tests/McjCoderOrg.HookTests/StepDefinitions/CommonHookSteps.cs index 0c8b106..d8b6cfe 100644 --- a/tests/McjCoderOrg.HookTests/StepDefinitions/CommonHookSteps.cs +++ b/tests/McjCoderOrg.HookTests/StepDefinitions/CommonHookSteps.cs @@ -7,13 +7,15 @@ namespace McjCoderOrg.HookTests.StepDefinitions; public sealed class CommonHookSteps : IAsyncDisposable { private readonly ScenarioContext _scenarioContext; + private readonly IReqnrollOutputHelper _output; private GitRepositoryFixture? _fixture; private HookRunner? _hookRunner; private HookResult? _lastResult; - public CommonHookSteps(ScenarioContext scenarioContext) + public CommonHookSteps(ScenarioContext scenarioContext, IReqnrollOutputHelper output) { _scenarioContext = scenarioContext; + _output = output; } internal GitRepositoryFixture Fixture => _fixture ?? throw new InvalidOperationException("Fixture not initialized"); @@ -28,6 +30,7 @@ internal void SetLastResult(HookResult result) [Given("I have a git repository with hooks configured")] public async Task GivenIHaveAGitRepositoryWithHooksConfigured() { + _output.WriteLine("Setting up git repository with hooks"); ToolAvailability.SkipIfGitBashMissing(); _fixture = new GitRepositoryFixture(); @@ -35,7 +38,11 @@ public async Task GivenIHaveAGitRepositoryWithHooksConfigured() // Get the source paths from the solution root var sourceProjectPath = FindSourceProjectPath(); var sourceHuskyPath = Path.Combine(sourceProjectPath, ".husky"); + _output.WriteLine("Source project path: {0}", sourceProjectPath); + _output.WriteLine("Husky path: {0}", sourceHuskyPath); + await _fixture.InitializeAsync(sourceHuskyPath).ConfigureAwait(false); + _output.WriteLine("Repository initialized at: {0}", _fixture.RepoPath); _hookRunner = new HookRunner(_fixture.RepoPath, _fixture.HuskyPath); @@ -50,30 +57,31 @@ public async Task GivenIHaveAGitRepositoryWithHooksConfigured() [Given("I am on the {string} branch")] public async Task GivenIAmOnTheBranch(string branchName) { + _output.WriteLine("Switching to branch: {0}", branchName); await Fixture.SwitchToBranchAsync(branchName).ConfigureAwait(false); } [Given("I am on a {string} branch")] - public async Task GivenIAmOnABranch(string branchName) - { - await Fixture.SwitchToBranchAsync(branchName).ConfigureAwait(false); - } + public Task GivenIAmOnABranch(string branchName) => GivenIAmOnTheBranch(branchName); [Given("I am in detached HEAD state")] public async Task GivenIAmInDetachedHeadState() { + _output.WriteLine("Detaching HEAD"); await Fixture.DetachHeadAsync().ConfigureAwait(false); } [Given("GPG signing is configured")] public async Task GivenGpgSigningIsConfigured() { + _output.WriteLine("Configuring GPG signing"); await Fixture.ConfigureGpgSigningAsync(enabled: true).ConfigureAwait(false); } [Given("GPG signing is not configured")] public async Task GivenGpgSigningIsNotConfigured() { + _output.WriteLine("Disabling GPG signing"); await Fixture.ConfigureGpgSigningAsync(enabled: false).ConfigureAwait(false); } @@ -100,24 +108,33 @@ public void GivenNoSlnFileExists() [Then("the hook should fail with exit code {int}")] public void ThenTheHookShouldFailWithExitCode(int expectedExitCode) { + _output.WriteLine("Verifying exit code: expected={0}, actual={1}", expectedExitCode, LastResult.ExitCode); LastResult.ExitCode.Should().Be(expectedExitCode, "Expected hook to fail with exit code {0}", expectedExitCode); } [Then("the hook should fail")] public void ThenTheHookShouldFail() { + _output.WriteLine("Verifying hook failed: exit code={0}", LastResult.ExitCode); LastResult.ExitCode.Should().NotBe(0, "Expected hook to fail (exit code != 0)"); } [Then("the hook should succeed")] public void ThenTheHookShouldSucceed() { + _output.WriteLine("Verifying hook succeeded: exit code={0}", LastResult.ExitCode); + if (LastResult.ExitCode != 0) + { + _output.WriteLine("Hook output: {0}", LastResult.CombinedOutput); + } + LastResult.ExitCode.Should().Be(0, "Expected hook to succeed but got exit code {0}.\nOutput: {1}", LastResult.ExitCode, LastResult.CombinedOutput); } [Then("the output should contain {string}")] public void ThenTheOutputShouldContain(string expected) { + _output.WriteLine("Verifying output contains: {0}", expected); LastResult.CombinedOutput.Should().Contain(expected); } diff --git a/tests/McjCoderOrg.HookTests/StepDefinitions/PreCommitHookSteps.cs b/tests/McjCoderOrg.HookTests/StepDefinitions/PreCommitHookSteps.cs index f17ab38..e3cd61b 100644 --- a/tests/McjCoderOrg.HookTests/StepDefinitions/PreCommitHookSteps.cs +++ b/tests/McjCoderOrg.HookTests/StepDefinitions/PreCommitHookSteps.cs @@ -7,10 +7,12 @@ namespace McjCoderOrg.HookTests.StepDefinitions; public sealed class PreCommitHookSteps { private readonly ScenarioContext _scenarioContext; + private readonly IReqnrollOutputHelper _output; - public PreCommitHookSteps(ScenarioContext scenarioContext) + public PreCommitHookSteps(ScenarioContext scenarioContext, IReqnrollOutputHelper output) { _scenarioContext = scenarioContext; + _output = output; } private GitRepositoryFixture Fixture => (GitRepositoryFixture)_scenarioContext["Fixture"]; @@ -20,10 +22,17 @@ public PreCommitHookSteps(ScenarioContext scenarioContext) [When("I attempt to commit")] public async Task WhenIAttemptToCommit() { + _output.WriteLine("Staging test file and running pre-commit hook"); // Stage a test file if nothing is staged await Fixture.StageFileAsync("test.txt", $"Test content {DateTime.UtcNow:O}").ConfigureAwait(false); var result = await HookRunner.RunHookAsync("pre-commit").ConfigureAwait(false); + _output.WriteLine("Pre-commit exit code: {0}", result.ExitCode); + if (!string.IsNullOrEmpty(result.CombinedOutput)) + { + _output.WriteLine("Pre-commit output: {0}", result.CombinedOutput); + } + CommonSteps.SetLastResult(result); } } diff --git a/tests/McjCoderOrg.HookTests/StepDefinitions/PrePushHookSteps.cs b/tests/McjCoderOrg.HookTests/StepDefinitions/PrePushHookSteps.cs index 2d3aeed..de2748a 100644 --- a/tests/McjCoderOrg.HookTests/StepDefinitions/PrePushHookSteps.cs +++ b/tests/McjCoderOrg.HookTests/StepDefinitions/PrePushHookSteps.cs @@ -6,10 +6,12 @@ namespace McjCoderOrg.HookTests.StepDefinitions; public sealed class PrePushHookSteps { private readonly ScenarioContext _scenarioContext; + private readonly IReqnrollOutputHelper _output; - public PrePushHookSteps(ScenarioContext scenarioContext) + public PrePushHookSteps(ScenarioContext scenarioContext, IReqnrollOutputHelper output) { _scenarioContext = scenarioContext; + _output = output; } private HookRunner HookRunner => (HookRunner)_scenarioContext["HookRunner"]; @@ -18,6 +20,7 @@ public PrePushHookSteps(ScenarioContext scenarioContext) [Given("dotnet CLI is not available")] public void GivenDotnetCliIsNotAvailable() { + _output.WriteLine("Marking dotnet CLI as unavailable"); // This is a state marker - the actual behavior depends on system config // We can't actually make dotnet unavailable, but we can verify the hook handles it _scenarioContext["DotnetUnavailable"] = true; @@ -26,8 +29,15 @@ public void GivenDotnetCliIsNotAvailable() [When("I attempt to push")] public async Task WhenIAttemptToPush() { + _output.WriteLine("Running pre-push hook"); // The pre-push hook validates branch naming, not actual push var result = await HookRunner.RunHookAsync("pre-push").ConfigureAwait(false); + _output.WriteLine("Pre-push exit code: {0}", result.ExitCode); + if (!string.IsNullOrEmpty(result.CombinedOutput)) + { + _output.WriteLine("Pre-push output: {0}", result.CombinedOutput); + } + CommonSteps.SetLastResult(result); } }