Skip to content

feat(tests): Add Nodsoft.MoltenObsidian.Tool.Tests covering CLI commands end-to-end#72

Open
Copilot wants to merge 3 commits intodevelopfrom
copilot/add-cli-tool-tests
Open

feat(tests): Add Nodsoft.MoltenObsidian.Tool.Tests covering CLI commands end-to-end#72
Copilot wants to merge 3 commits intodevelopfrom
copilot/add-cli-tool-tests

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 10, 2026

No test coverage existed for the CLI tool (Nodsoft.MoltenObsidian.Tool). This PR adds a dedicated test project exercising all validation branches and core command logic for both manifest generate and ssg generate.

Project setup

  • New Nodsoft.MoltenObsidian.Tool.Tests targeting net8.0;net10.0, mirroring existing test project conventions (Microsoft.NET.Sdk.Web, OutputType=Exe, xunit.v3)
  • InternalsVisibleTo added in Directory.Build.props to expose internal static command methods
  • Registered in Nodsoft.MoltenObsidian.slnx

Shared fixture

VaultFixture — creates a temp directory, seeds a minimal Obsidian vault (.obsidian/ + a few .md notes), and deletes it on dispose. Reused across all test classes.

Settings validation tests

Direct instantiation of CommandSettings subclasses — no process needed.

  • GenerateManifestSettingsTests (7 tests): empty/missing vault path, non-existent path, no .obsidian with and without --force, valid+invalid output paths
  • GenerateStaticSiteCommandSettingsTests (11 tests): --from-folder/--from-url mutual exclusion, non-existent local path, invalid/unsupported/valid URL schemes, --generate-manifest + remote URL conflict, --watch without local vault, valid/non-existent output path

GenerateStaticSiteCommandSettings uses private setters — a small reflection-based SettingsBuilder helper populates them without going through Spectre's parse pipeline.

Command logic tests

Calls internal static methods directly against real temp directories.

  • GenerateManifestCommandTests (5 tests): manifest file is written, contains seeded files, promptOverwrite=false preserves existing content, promptOverwrite=true overwrites with valid JSON, non-FileSystemVault throws InvalidOperationException
  • GenerateStaticSiteCommandTests (3 tests): WriteStaticFilesAsync produces .html output, ignored files are excluded, ignored folders are excluded (vault constructed with custom ignore list, reflecting that folder filtering occurs at vault-construction time rather than in IsIgnored)
Original prompt

Add CLI Tool Tests

Create a new test project Nodsoft.MoltenObsidian.Tool.Tests that covers the CLI tool's full range of features and ensures deterministic behaviour.

Context

The CLI tool (Nodsoft.MoltenObsidian.Tool) is built with Spectre.Console.Cli and exposes two command branches:

  • manifest generateGenerateManifestCommand / GenerateManifestSettings
  • ssg generateGenerateStaticSite / GenerateStaticSiteCommandSettings

The existing test projects (Nodsoft.MoltenObsidian.Tests, Nodsoft.MoltenObsidian.Blazor.Tests) use xunit.v3 and serve as reference for project setup.

What to implement

1. New test project: Nodsoft.MoltenObsidian.Tool.Tests

Create Nodsoft.MoltenObsidian.Tool.Tests/Nodsoft.MoltenObsidian.Tool.Tests.csproj:

  • Target net8.0;net10.0
  • Use xunit.v3, xunit.v3.runner.msbuild, Microsoft.NET.Test.Sdk, coverlet.collector (same versions as existing test projects)
  • Reference Nodsoft.MoltenObsidian.Tool.csproj
  • Add the project to the solution file (Nodsoft.MoltenObsidian.slnx)

2. Settings validation tests

Both CommandSettings subclasses have pure Validate() methods. Test all branches:

GenerateManifestSettingsTests (in Commands/Manifest/):

  • Valid vault path with .obsidian folder → success
  • Empty/missing vault path → error
  • Path does not exist → error
  • Vault exists but no .obsidian folder, Force = false → error
  • Vault exists but no .obsidian folder, Force = true → success
  • Valid vault path + valid existing output path → success
  • Valid vault path + non-existent output path → error

GenerateStaticSiteCommandSettingsTests (in Commands/SSG/):

  • --from-folder and --from-url both set → error
  • --from-folder with non-existent path → error
  • --from-folder with valid path → success
  • --from-url with invalid URL → error
  • --from-url with unsupported scheme (e.g. file://) → error
  • --from-url with valid http:// URL → success
  • --from-url with valid ftp:// URL → success
  • --generate-manifest with --from-url → error
  • --watch without a local vault (--from-url) → error
  • Valid --output-path that exists → success
  • --output-path that does not exist → error

3. Command logic unit tests

Both commands expose internal static async methods. Test them using real temp directories:

GenerateManifestCommandTests (in Commands/Manifest/):

  • Generates a manifest file at the output path
  • Manifest JSON contains the vault's seeded files
  • When manifest file already exists and promptOverwrite returns false → file is not overwritten (content unchanged)
  • When manifest file already exists and promptOverwrite returns true → file is overwritten
  • Passing a non-FileSystemVault throws InvalidOperationException

GenerateStaticSiteCommandTests (in Commands/SSG/):

  • WriteStaticFilesAsync writes expected output files to the output directory for a seeded local vault
  • Ignored files are not written to the output directory
  • Ignored folders are not written to the output directory

4. Test fixtures / helpers

Create a VaultFixture (or equivalent IAsyncLifetime class) that:

  • Creates a temp directory
  • Seeds a minimal valid Obsidian vault (.obsidian/ folder + a few .md files)
  • Cleans up on dispose

Use this fixture across test classes to avoid duplication.

Constraints

  • Do not use System.Diagnostics.Process for E2E tests — test at the unit/integration level using direct method calls and settings instantiation.
  • Use Directory.CreateTempSubdirectory for temp dirs.
  • All test classes must clean up temp directories in Dispose / DisposeAsync.
  • Follow the same code style and conventions as the existing codebase (file-scoped namespaces, [UsedImplicitly] where appropriate, etc.).
  • The internal static methods in the command classes are already accessible — use InternalsVisibleTo in the Tool project's .csproj if needed.

The following is the prior conversation context from the user's chat exploration (may be truncated):

User: How could we implement tests for a CLI tool, so as to test its functionality and predictable behaviour ?

Assistant: Now I have a thorough understanding of the codebase. Here's a comprehensive strategy for testing this CLI tool:


CLI Testing Strategy for Nodsoft.MoltenObsidian.Tool

The tool uses Spectre.Console.Cli and is structured around two commands (manifest generate, ssg generate). There are three layers worth testing:


1. New Test Project: Nodsoft.MoltenObsidian.Tool.Tests

Create a dedicated test project alongside the existing ones, referencing the Tool project. Mirror the existing test project's setup:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>net8.0;net10.0</TargetFrameworks>
    <IsPackable>false</IsPackable>
    <IsTestProject>true</IsTestPro...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

*This pull request was created from Copilot chat.*
>

Copilot AI and others added 2 commits April 10, 2026 21:41
…l coverage

Agent-Logs-Url: https://github.com/Nodsoft/MoltenObsidian/sessions/2e978ebb-d1bc-4fc0-8665-9c59f41766a8

Co-authored-by: SakuraIsayeki <4705518+SakuraIsayeki@users.noreply.github.com>
… manifest tests

Agent-Logs-Url: https://github.com/Nodsoft/MoltenObsidian/sessions/2e978ebb-d1bc-4fc0-8665-9c59f41766a8

Co-authored-by: SakuraIsayeki <4705518+SakuraIsayeki@users.noreply.github.com>
Copilot AI changed the title [WIP] Add tests for CLI tool features feat(tests): Add Nodsoft.MoltenObsidian.Tool.Tests covering CLI commands end-to-end Apr 10, 2026
Copilot AI requested a review from SakuraIsayeki April 10, 2026 21:46
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a dedicated xUnit v3 test project to exercise the Nodsoft.MoltenObsidian.Tool CLI command settings validation and core command logic without spinning up external processes, improving coverage for manifest generate and ssg generate.

Changes:

  • Added new Nodsoft.MoltenObsidian.Tool.Tests project (multi-targeting via repo props) and registered it in the solution.
  • Introduced a temp-vault VaultFixture plus settings-validation tests for manifest + SSG commands.
  • Added integration-style tests that call the tool’s internal static methods to generate manifests and static site output.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
Nodsoft.MoltenObsidian.Tool.Tests/Nodsoft.MoltenObsidian.Tool.Tests.csproj New xUnit v3 test project referencing the CLI tool and required vault implementations
Nodsoft.MoltenObsidian.Tool.Tests/VaultFixture.cs Shared temp-vault seeding + cleanup fixture for command tests
Nodsoft.MoltenObsidian.Tool.Tests/Commands/Manifest/GenerateManifestSettingsTests.cs Branch coverage for GenerateManifestSettings.Validate()
Nodsoft.MoltenObsidian.Tool.Tests/Commands/Manifest/GenerateManifestCommandTests.cs Tests for manifest generation/overwrite behavior and vault-type enforcement
Nodsoft.MoltenObsidian.Tool.Tests/Commands/SSG/GenerateStaticSiteCommandSettingsTests.cs Validation tests for GenerateStaticSiteCommandSettings (incl. reflection-based builder)
Nodsoft.MoltenObsidian.Tool.Tests/Commands/SSG/GenerateStaticSiteCommandTests.cs Tests for .html generation and ignore behavior
Directory.Build.props Added InternalsVisibleTo entry for the new test assembly
Nodsoft.MoltenObsidian.slnx Added the new test project to the solution

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

);

Assert.NotEmpty(manifest.Files);
Assert.Contains(manifest.Files, f => f.Path.EndsWith("Note.md"));
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion EndsWith("Note.md") is ambiguous here because multiple seeded files end with the same suffix (e.g., "Another Note.md"). This can let the test pass even if the specific expected file is missing. Prefer asserting an exact relative path match (or asserting the full expected set of paths) to avoid false positives.

Suggested change
Assert.Contains(manifest.Files, f => f.Path.EndsWith("Note.md"));
Assert.Contains(manifest.Files, f => string.Equals(f.Path, "Note.md", StringComparison.Ordinal));

Copilot uses AI. Check for mistakes.
// The seeded vault has Note.md, Another Note.md, and SubFolder/Nested Note.md.
// WriteStaticFilesAsync converts notes to .html files.
string[] htmlFiles = Directory.GetFiles(_outputDir.FullName, "*.html", SearchOption.AllDirectories);
Assert.NotEmpty(htmlFiles);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only asserts that at least one .html file is produced, which could pass even if only a subset of notes are emitted or filenames/paths are wrong. To make the test deterministic, assert the expected output filenames (e.g., Note.html, Another Note.html, SubFolder/Nested Note.html) and/or the expected count.

Suggested change
Assert.NotEmpty(htmlFiles);
for (int i = 0; i < htmlFiles.Length; i++)
{
htmlFiles[i] = Path.GetRelativePath(_outputDir.FullName, htmlFiles[i]).Replace(Path.DirectorySeparatorChar, '/');
}
Array.Sort(htmlFiles, StringComparer.Ordinal);
string[] expectedHtmlFiles =
[
"Another Note.html",
"Note.html",
"SubFolder/Nested Note.html",
];
Array.Sort(expectedHtmlFiles, StringComparer.Ordinal);
Assert.Equal(expectedHtmlFiles, htmlFiles);

Copilot uses AI. Check for mistakes.
System.Reflection.PropertyInfo? prop = obj.GetType().GetProperty(propertyName,
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

prop?.SetValue(obj, value);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetPrivate silently does nothing if the property lookup fails (prop?), which can lead to false positives (e.g., a test expecting success might still pass even if the intended property was never set). Consider failing fast by throwing/asserting when the property is missing or not writable so the tests reliably exercise the intended validation branches.

Suggested change
prop?.SetValue(obj, value);
if (prop is null)
{
throw new InvalidOperationException($"Property '{propertyName}' was not found on type '{obj.GetType().FullName}'.");
}
System.Reflection.MethodInfo? setMethod = prop.GetSetMethod(nonPublic: true);
if (!prop.CanWrite || setMethod is null)
{
throw new InvalidOperationException($"Property '{propertyName}' on type '{obj.GetType().FullName}' is not writable.");
}
prop.SetValue(obj, value);

Copilot uses AI. Check for mistakes.
@SakuraIsayeki SakuraIsayeki marked this pull request as ready for review April 10, 2026 21:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants