diff --git a/docs/design/command-line.md b/docs/design/command-line.md index 22e9d47..705bda2 100644 --- a/docs/design/command-line.md +++ b/docs/design/command-line.md @@ -34,12 +34,12 @@ generate event-log entries. This satisfies requirements `VersionMark-Cmd-ExitCod | Priority | Condition | Action | |----------|------------------------|---------------------------------| | 1 | `context.Version` | Print version string and return | -| 2 | Print banner | Always executed after priority 1| -| 3 | `context.Help` | Print usage and return | -| 4 | `context.Validate` | Run self-validation and return | -| 5 | `context.Capture` | Run capture mode and return | -| 5.5 | `context.Publish` | Run publish mode and return | -| 6 | Default | Run placeholder tool logic | +| — | Print banner | Always executed after priority 1| +| 2 | `context.Help` | Print usage and return | +| 3 | `context.Validate` | Run self-validation and return | +| 4 | `context.Capture` | Run capture mode and return | +| 4.5 | `context.Publish` | Run publish mode and return | +| 5 | Default | Run placeholder tool logic | This dispatch order satisfies requirements `VersionMark-Cmd-Version`, `VersionMark-Cmd-Help`, `VersionMark-Cmd-Validate`, `VersionMark-Cap-Capture`, and `VersionMark-Pub-Publish`. diff --git a/docs/design/validation.md b/docs/design/validation.md index 83dfdbc..b5a78b7 100644 --- a/docs/design/validation.md +++ b/docs/design/validation.md @@ -30,7 +30,8 @@ organizes all test execution internally. 1. Creates a `TemporaryDirectory` (see below). 2. Writes a minimal `.versionmark.yaml` containing only the `dotnet` tool. -3. Constructs a `Context` with `--capture`, `--job-id test-job`, and `--output `. +3. Constructs a `Context` with `--silent`, `--log `, `--capture`, `--job-id test-job`, + and `--output `. 4. Changes the current directory to the temp directory and calls `Program.Run`. 5. Verifies exit code is 0, output file exists, `JobId` equals `"test-job"`, and `dotnet` version was captured and is non-empty. @@ -43,7 +44,8 @@ The test name is `VersionMark_CapturesVersions`, satisfying `VersionMark-Cap-Cap 1. Creates a `TemporaryDirectory`. 2. Writes two `VersionInfo` JSON files with known content. -3. Constructs a `Context` with `--publish`, `--report `, and `-- versionmark-*.json`. +3. Constructs a `Context` with `--silent`, `--log `, `--publish`, `--report `, + `--report-depth 2`, and `-- versionmark-*.json`. 4. Changes the current directory to the temp directory and calls `Program.Run`. 5. Verifies exit code is 0, report file exists, and contains `## Tool Versions`, `**dotnet**`, `**node**`, `8.0.0`, and `20.0.0`. diff --git a/docs/design/version-info.md b/docs/design/version-info.md index d2d9844..dff325c 100644 --- a/docs/design/version-info.md +++ b/docs/design/version-info.md @@ -20,7 +20,8 @@ The `VersionInfo` record (`VersionInfo.cs`) is a positional record with two prop `SaveToFile` serializes the record to indented JSON using `JsonSerializer.Serialize` with `WriteIndented = true` and writes it to the specified path using UTF-8 encoding. Non-`InvalidOperationException` errors are wrapped and re-thrown as `InvalidOperationException` with context. This satisfies -requirements `VersionMark-Cap-JsonOutput` and `VersionMark-Cap-DefaultOutput`. +requirement `VersionMark-Cap-JsonOutput`. The default output filename (`versionmark-.json`) +is determined by the CLI layer and contributes to satisfying `VersionMark-Cap-DefaultOutput`. ### LoadFromFile Method diff --git a/docs/reqstream/capture.yaml b/docs/reqstream/capture.yaml index c27dc77..84cd1a7 100644 --- a/docs/reqstream/capture.yaml +++ b/docs/reqstream/capture.yaml @@ -12,7 +12,6 @@ sections: tags: - capture tests: - - VersionMark_CapturesVersions - Program_Run_WithCaptureCommand_CapturesToolVersions - IntegrationTest_CaptureCommand_CapturesToolVersions diff --git a/docs/reqstream/command-line.yaml b/docs/reqstream/command-line.yaml index eec1292..a240bee 100644 --- a/docs/reqstream/command-line.yaml +++ b/docs/reqstream/command-line.yaml @@ -94,12 +94,22 @@ sections: - id: VersionMark-Cmd-ErrorOutput title: The tool shall write error messages to stderr. + justification: | + Separating error output from standard output allows callers and CI/CD + pipelines to distinguish normal output from failure diagnostics. + tags: + - cli tests: - Context_WriteError_NotSilent_WritesToConsole - IntegrationTest_UnknownArgument_ReturnsError - id: VersionMark-Cmd-InvalidArgs title: The tool shall reject unknown or malformed command-line arguments with a descriptive error. + justification: | + Clear rejection of invalid arguments prevents silent misconfiguration and + guides users toward correct usage without requiring external documentation. + tags: + - cli tests: - Context_Create_UnknownArgument_ThrowsArgumentException - Context_Create_LogFlag_WithoutValue_ThrowsArgumentException @@ -108,6 +118,11 @@ sections: - id: VersionMark-Cmd-ExitCode title: The tool shall return a non-zero exit code on failure. + justification: | + A non-zero exit code on failure enables CI/CD pipelines and scripts to + detect and respond to tool failures automatically. + tags: + - cli tests: - Context_WriteError_SetsErrorExitCode - IntegrationTest_UnknownArgument_ReturnsError diff --git a/docs/reqstream/formatter.yaml b/docs/reqstream/formatter.yaml index 56f6ba2..7d3671b 100644 --- a/docs/reqstream/formatter.yaml +++ b/docs/reqstream/formatter.yaml @@ -41,7 +41,7 @@ sections: - id: VersionMark-Fmt-MarkdownList title: >- - The tool shall use subscript formatting and parenthesized job IDs to indicate which + The tool shall use bold formatting and parenthesized job IDs to indicate which jobs had which versions. justification: | Provides clear visual distinction between different job executions and their diff --git a/docs/reqstream/publish.yaml b/docs/reqstream/publish.yaml index 6f761c6..27ce6f7 100644 --- a/docs/reqstream/publish.yaml +++ b/docs/reqstream/publish.yaml @@ -12,7 +12,7 @@ sections: tags: - publish tests: - - VersionMark_GeneratesMarkdownReport + - VersionMark_PublishCommand_GeneratesMarkdownReport - id: VersionMark-Pub-Report title: The tool shall support --report parameter to specify the output markdown file path in publish mode. @@ -22,7 +22,7 @@ sections: tags: - publish tests: - - VersionMark_GeneratesMarkdownReport + - VersionMark_PublishCommand_GeneratesMarkdownReport - id: VersionMark-Pub-ReportDepth title: The tool shall support --report-depth parameter to control heading depth in generated markdown. @@ -52,7 +52,8 @@ sections: tags: - publish tests: - - VersionMark_GeneratesMarkdownReport + - VersionMark_PublishCommandWithCustomGlobPatterns_FiltersFiles + - Context_Create_GlobPatternsAfterSeparator_CapturesPatterns - id: VersionMark-Pub-Consolidate title: The tool shall read and parse JSON files matching the specified glob patterns in publish mode. @@ -62,7 +63,7 @@ sections: tags: - publish tests: - - VersionMark_GeneratesMarkdownReport + - VersionMark_PublishCommand_GeneratesMarkdownReport - id: VersionMark-Pub-ConflictReport title: The tool shall report errors when no JSON files match the specified glob patterns. diff --git a/src/DemaConsulting.VersionMark/VersionMarkConfig.cs b/src/DemaConsulting.VersionMark/VersionMarkConfig.cs index fa69c35..7be6e78 100644 --- a/src/DemaConsulting.VersionMark/VersionMarkConfig.cs +++ b/src/DemaConsulting.VersionMark/VersionMarkConfig.cs @@ -331,8 +331,9 @@ public VersionInfo FindVersions(IEnumerable toolNames, string jobId) /// The combined stdout and stderr output. /// Thrown when the command fails to execute. /// - /// Commands are split on the first space character to separate executable from arguments. - /// This does not handle quoted arguments containing spaces. + /// Commands are delegated to the OS shell (cmd.exe /c on Windows, /bin/sh -c + /// elsewhere) via ArgumentList to avoid escaping issues. This supports .cmd/.bat + /// files on Windows and shell features (pipes, redirects, built-ins) on all platforms. /// private static string RunCommand(string command) { diff --git a/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs b/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs index 3e5cff7..9495840 100644 --- a/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/IntegrationTests.cs @@ -196,7 +196,7 @@ public void IntegrationTest_SilentFlag_SuppressesOutput() { // Arrange & Act - Run the application with --silent flag var exitCode = Runner.Run( - out var _, + out var output, "dotnet", _dllPath, "--silent"); @@ -204,7 +204,9 @@ public void IntegrationTest_SilentFlag_SuppressesOutput() // Assert - Verify success Assert.AreEqual(0, exitCode); - // Output check removed since silent mode may still produce some output + // Verify the tool's normal output is suppressed + Assert.DoesNotContain("VersionMark version", output); + Assert.DoesNotContain("Copyright (c) DEMA Consulting", output); } /// diff --git a/test/DemaConsulting.VersionMark.Tests/ProgramTests.cs b/test/DemaConsulting.VersionMark.Tests/ProgramTests.cs index 05115cf..fa66a1d 100644 --- a/test/DemaConsulting.VersionMark.Tests/ProgramTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/ProgramTests.cs @@ -45,6 +45,7 @@ public void Program_Run_WithVersionFlag_DisplaysVersionOnly() // Assert - Verify version-only output var output = outWriter.ToString(); + Assert.IsFalse(string.IsNullOrWhiteSpace(output), "Version string should be printed"); Assert.DoesNotContain("Copyright", output); Assert.DoesNotContain("VersionMark version", output); } diff --git a/test/DemaConsulting.VersionMark.Tests/VersionInfoTests.cs b/test/DemaConsulting.VersionMark.Tests/VersionInfoTests.cs index c3a461f..c61569b 100644 --- a/test/DemaConsulting.VersionMark.Tests/VersionInfoTests.cs +++ b/test/DemaConsulting.VersionMark.Tests/VersionInfoTests.cs @@ -278,6 +278,31 @@ public void VersionInfo_EmptyVersions_SavesAndLoadsCorrectly() } } + /// + /// Test LoadFromFile throws ArgumentException when JSON deserializes to null (e.g., literal "null"). + /// + [TestMethod] + public void VersionInfo_LoadFromFile_NullJson_ThrowsArgumentException() + { + // Arrange + var tempFile = Path.GetTempFileName(); + try + { + File.WriteAllText(tempFile, "null"); + + // Act & Assert + var exception = Assert.Throws(() => VersionInfo.LoadFromFile(tempFile)); + Assert.Contains("deserialize", exception.Message, StringComparison.OrdinalIgnoreCase); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + /// /// Test VersionInfo with special characters in values. ///