Skip to content

feat(dotnet): add core reporter (listener, collector, writer, SPI hook) - #194

Merged
nizos merged 1 commit into
nizos:mainfrom
edblackburn:feat/dotnet-core
Aug 16, 2026
Merged

feat(dotnet): add core reporter (listener, collector, writer, SPI hook)#194
nizos merged 1 commit into
nizos:mainfrom
edblackburn:feat/dotnet-core

Conversation

@edblackburn

Copy link
Copy Markdown
Contributor

Summary

Adds the .NET reporter core: an MTP V2 (Microsoft Testing Platform) in-process extension that captures test results and writes .claude/tdd-guard/data/test.json. Framework-agnostic — any MTP V2-hosted framework works without reporter changes.

Scope

Following your guidance on issue #84 ("keeping the PR focused and the initial implementation simple will make it much easier for me to review") and the shape you accepted for the JUnit5 reporter (#160 — listener, collector, JSON writer, SPI/hook registration, build wrapper, unit tests only), this PR is scoped identically:

  • TddGuard.Dotnet.Core/ — pure domain types and functions, no MTP dependency
  • TddGuard.Dotnet/ — the MTP V2 extension (listener + builder hook)
  • TddGuard.Dotnet.Tests/ — unit and property-based tests only

No src/ changes. No CI changes (separate PR, stacked on this one). No integration test factory or framework compatibility smoke tests (separate PRs, same as JUnit5's follow-up sequence).

Design

  • Domain logic (Core) has zero MTP dependency — testable without a test host
  • Errors are values: ProjectRootResolver.Resolve() returns OneOf<ProjectRoot, ResolveError>, no exceptions for flow control
  • Per ADR-010: when neither TDD_GUARD_PROJECT_ROOT nor CLAUDE_PROJECT_DIR is set, the extension disables itself and logs to stderr — it does not silently fall back to cwd
  • Cross-platform path handling: Path.GetFullPath() + normalized comparison for the ancestor-of-cwd check (handles Windows separators, case-insensitivity, trailing separators, macOS /var/private/var symlinks)

Testing

70 unit tests, including property-based roundtrip serialization (FsCheck). All pass on .NET 10.

Distribution

Planned for NuGet.org, published by you — following up separately on issue #84.

Stacked PRs

This is the first of a stack of 4 PRs decomposing the reporter the way JUnit5 landed:

  1. This PR — core reporter
  2. feat/dotnet-ci → CI build step
  3. feat/dotnet-integration-tests → Node integration test factory + minimal fixtures
  4. feat/dotnet-framework-compat → framework compatibility smoke tests

Each is based on the previous, so they'll show as small incremental diffs once opened.

Adds the .NET reporter core: an MTP V2 (Microsoft Testing Platform)
in-process extension that captures test results and writes
.claude/tdd-guard/data/test.json. Framework-agnostic - any MTP V2-
hosted framework works without reporter changes.

Following the guidance on issue nizos#84 ("keeping the PR focused and the
initial implementation simple will make it much easier for me to
review") and the shape accepted for the JUnit5 reporter (nizos#160 -
listener, collector, JSON writer, SPI/hook registration, build
wrapper, unit tests only), this PR is scoped identically:

- TddGuard.Dotnet.Core/ - pure domain types and functions, no MTP
  dependency
- TddGuard.Dotnet/ - the MTP V2 extension (listener + builder hook)
- TddGuard.Dotnet.Tests/ - unit and property-based tests only

No src/ changes. No CI changes (separate PR). No integration test
factory or framework compatibility smoke tests (separate PRs, same
as JUnit5's follow-up sequence).

Design:
- Domain logic (Core) has zero MTP dependency - testable without a
  test host
- Errors are values: ProjectRootResolver.Resolve() returns
  OneOf<ProjectRoot, ResolveError>, no exceptions for flow control
- Per ADR-010: when neither TDD_GUARD_PROJECT_ROOT nor
  CLAUDE_PROJECT_DIR is set, the extension disables itself and logs
  to stderr - it does not silently fall back to cwd
- Cross-platform path handling: Path.GetFullPath() + normalized
  comparison for the ancestor-of-cwd check (handles Windows
  separators, case-insensitivity, trailing separators, macOS
  /var -> /private/var symlinks)

Testing: 70 unit tests, including property-based roundtrip
serialization (FsCheck). All pass on .NET 10.

Distribution: Planned for NuGet.org, published by the maintainer -
following up separately on issue nizos#84.
@nizos

nizos commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Hi @edblackburn,

Thanks for the PR! I'm back from vacation and should be able to look at it this weekend. Talk to you soon! :)

@nizos nizos closed this Aug 16, 2026
@nizos nizos reopened this Aug 16, 2026
@nizos
nizos merged commit e48f971 into nizos:main Aug 16, 2026
19 of 20 checks passed
@nizos

nizos commented Aug 16, 2026

Copy link
Copy Markdown
Owner

Hi @edblackburn,

Thanks for the PR and great work! I closed and re-opened the PR just to get the npm audit check to pick up a dependency fix that landed on main earlier today.

I will merge this and follow up with dotnet support in the devcontainer.

A few things that I would like your read on before we publish:

  • Timed out and cancelled tests currently serialize as passed. They land in the _ => Passed() catch-all in ConsumeAsync. Should we map both to failed with a message? Fail closed feels right for TDD Guard. Could the FsCheck generator grow to cover those states too?
  • Should we pin the floating package versions (OneOf 3.*, TUnit 0.*, FsCheck 3.*)?
  • On packaging shape, TddGuard.Dotnet.Core is packable and referenced by the main project, so packing produces a second package that also needs publishing, unless I missed something. I would prefer to publish a single TddGuard.Dotnet package. How would you approach that?
  • There is no solution file today, so the README's dotnet build from reporters/dotnet fails with MSB1003. Should we add one or update the command? Currently dotnet run --project TddGuard.Dotnet.Tests works for me.
  • Can we take a look at HandlesConcurrentConsumeAsyncCallsSafely? It blocks 100 thread pool threads on a Barrier and takes most of the suite time on my machine. I suspect small CI runners will feel it even more.
  • Two small notes: TddGuardListener.Version says 1.0.0 while the packages are 0.1.0, and .globalconfig mentions TestResultsWriter that I think is now ReportFileWriter.

Thanks again, this is outstanding work. I have now been looking forward to it and I'm glad to have you as a contributor. Excited for the rest of the stack so we can get it published. :)

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.

2 participants