diff --git a/tests/ScadaLink.CLI.Tests/Commands/AuditCommandTestHarness.cs b/tests/ScadaLink.CLI.Tests/Commands/AuditCommandTestHarness.cs
index 56ad835..4050ea7 100644
--- a/tests/ScadaLink.CLI.Tests/Commands/AuditCommandTestHarness.cs
+++ b/tests/ScadaLink.CLI.Tests/Commands/AuditCommandTestHarness.cs
@@ -25,15 +25,38 @@ internal static class AuditCommandTestHarness
return root;
}
+ ///
+ /// Parses and invokes the command tree, capturing output from both channels the CLI
+ /// uses: System.CommandLine's parser diagnostics flow through the
+ /// writers, while command actions write through
+ /// (consistent with the rest of the CLI). Both are merged into
+ /// the returned Out/Err strings. Callers must be in the Console
+ /// xUnit collection so the global redirect is not racy.
+ ///
public static (int Exit, string Out, string Err) Invoke(RootCommand root, params string[] args)
{
var output = new StringWriter();
var error = new StringWriter();
- var exit = root.Parse(args).Invoke(new InvocationConfiguration
+
+ var originalOut = Console.Out;
+ var originalErr = Console.Error;
+ Console.SetOut(output);
+ Console.SetError(error);
+ int exit;
+ try
{
- Output = output,
- Error = error,
- });
+ exit = root.Parse(args).Invoke(new InvocationConfiguration
+ {
+ Output = output,
+ Error = error,
+ });
+ }
+ finally
+ {
+ Console.SetOut(originalOut);
+ Console.SetError(originalErr);
+ }
+
return (exit, output.ToString(), error.ToString());
}
}
diff --git a/tests/ScadaLink.CLI.Tests/Commands/AuditExportCommandTests.cs b/tests/ScadaLink.CLI.Tests/Commands/AuditExportCommandTests.cs
index e802eb6..cd3349a 100644
--- a/tests/ScadaLink.CLI.Tests/Commands/AuditExportCommandTests.cs
+++ b/tests/ScadaLink.CLI.Tests/Commands/AuditExportCommandTests.cs
@@ -11,6 +11,7 @@ namespace ScadaLink.CLI.Tests.Commands;
/// required-flag enforcement, query-string construction, streaming the response body
/// to the output file, and the parquet-not-implemented (501) path.
///
+[Collection("Console")]
public class AuditExportCommandTests
{
// ---- CLI parsing: required flags --------------------------------------
diff --git a/tests/ScadaLink.CLI.Tests/Commands/AuditQueryCommandTests.cs b/tests/ScadaLink.CLI.Tests/Commands/AuditQueryCommandTests.cs
index 7773a90..130c8c7 100644
--- a/tests/ScadaLink.CLI.Tests/Commands/AuditQueryCommandTests.cs
+++ b/tests/ScadaLink.CLI.Tests/Commands/AuditQueryCommandTests.cs
@@ -12,6 +12,7 @@ namespace ScadaLink.CLI.Tests.Commands;
/// time-spec resolution, query-string construction, formatter selection, error
/// handling, and keyset-cursor paging via --all.
///
+[Collection("Console")]
public class AuditQueryCommandTests
{
// ---- Time-spec parsing -------------------------------------------------
diff --git a/tests/ScadaLink.CLI.Tests/Commands/AuditVerifyChainCommandTests.cs b/tests/ScadaLink.CLI.Tests/Commands/AuditVerifyChainCommandTests.cs
new file mode 100644
index 0000000..c399b92
--- /dev/null
+++ b/tests/ScadaLink.CLI.Tests/Commands/AuditVerifyChainCommandTests.cs
@@ -0,0 +1,58 @@
+using ScadaLink.CLI.Commands;
+
+namespace ScadaLink.CLI.Tests.Commands;
+
+///
+/// Tests for the scadalink audit verify-chain subcommand (Audit Log #23 M8-T4).
+/// v1 is a no-op stub: a valid --month prints the documented "not enabled"
+/// message and exits 0; a malformed month or a missing --month exits non-zero.
+///
+[Collection("Console")]
+public class AuditVerifyChainCommandTests
+{
+ [Fact]
+ public void VerifyChain_ValidMonth_ExitsZeroWithDocumentedMessage()
+ {
+ var root = AuditCommandTestHarness.BuildRoot();
+ var (exit, output, _) = AuditCommandTestHarness.Invoke(
+ root, "audit", "verify-chain", "--month", "2026-05");
+
+ Assert.Equal(0, exit);
+ Assert.Contains("Hash-chain tamper-evidence is not enabled", output);
+ Assert.Contains("Component-AuditLog.md", output);
+ }
+
+ [Fact]
+ public void VerifyChain_MalformedMonth_ExitsNonZero()
+ {
+ var root = AuditCommandTestHarness.BuildRoot();
+ var (exit, _, _) = AuditCommandTestHarness.Invoke(
+ root, "audit", "verify-chain", "--month", "2026-13");
+
+ Assert.NotEqual(0, exit);
+ }
+
+ [Fact]
+ public void VerifyChain_MissingMonth_ProducesRequiredFlagError()
+ {
+ var root = AuditCommandTestHarness.BuildRoot();
+ var (exit, _, err) = AuditCommandTestHarness.Invoke(root, "audit", "verify-chain");
+
+ Assert.NotEqual(0, exit);
+ Assert.Contains("--month", err);
+ }
+
+ [Theory]
+ [InlineData("2026-05", true)]
+ [InlineData("2026-01", true)]
+ [InlineData("2026-12", true)]
+ [InlineData("2026-13", false)]
+ [InlineData("2026-00", false)]
+ [InlineData("2026-5", false)]
+ [InlineData("not-a-month", false)]
+ [InlineData("", false)]
+ public void IsValidMonth_ValidatesYyyyMm(string month, bool expected)
+ {
+ Assert.Equal(expected, AuditVerifyChainHelpers.IsValidMonth(month));
+ }
+}