tdd-guard Documentation

Automated TDD enforcement for Claude Code

2,365 stars TypeScript TypeScript 37 files · ~28,043 tokens #agentic-coding#automation#claude-code#code-quality#hooks#llm-tools#tdd
RAW Doc

Technical Documentation: nizos/tdd-guard

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

TDD Guard





[!IMPORTANT]
TDD Guard grew into Probity: the same
TDD enforcement, now for Claude Code, Codex, and GitHub Copilot CLI, with
more reliable validation and no test reporters to set up. New projects should
start there. TDD Guard remains maintained for the projects that rely on it.

Automated Test-Driven Development enforcement for Claude Code.

TDD Guard ensures Claude Code follows Test-Driven Development principles. When your agent tries to skip tests or over-implement, TDD Guard blocks the action and explains what needs to happen instead.

Features

  • Test-First Enforcement - Blocks implementation without failing tests
  • Minimal Implementation - Prevents code beyond current test requirements
  • Lint Integration - Enforces refactoring using your linting rules
  • Customizable Rules - Adjust validation rules to match your TDD style
  • Flexible Validation - Choose faster or more capable models for your needs
  • Session Control - Toggle on and off mid-session

Getting Started

Requirements

  • Node.js 22+
  • A supported test framework (Vitest, Jest, Storybook, pytest, PHPUnit, Go, Rust, RSpec, Minitest)

Installation

Open Claude Code in your project and run:

  1. /plugin marketplace add nizos/tdd-guard
  2. /plugin install tdd-guard@tdd-guard
  3. /tdd-guard:setup

This adds the marketplace, installs the plugin, and configures the test reporter for your project. You may need to restart your terminal session or IDE extension for the setup skill to appear. For manual installation and configuration, see the installation guide.

Configuration

Security

TDD Guard hooks run with your user permissions. We maintain automated security scanning, dependency audits, and welcome source code review. See Claude Code's security considerations for more on hook safety.

Development

Contributing

Contributions are welcome! See the contributing guidelines to get started.

Contributors:

Support

  • Discussions - Ask questions and share ideas
  • Issues - Report bugs and request features

License

MIT


Overview (`.devcontainer/README.md`)

TDD Guard Development Container

A consistent, isolated environment for developing TDD Guard.

Features

  • Network isolation for security
  • Automated development environment setup
  • Pre-configured development tools
  • Persistent command history

What's Inside

Component Purpose
Node.js 22 Main CLI & Vitest/Jest/Storybook reporters
Python 3.11 + pipx Pytest reporter
PHP 8.2 + Composer PHPUnit reporter
Go 1.24 Go reporter
Rust 1.89 Rust reporter with cargo-nextest
Ruby 3.x + Bundler RSpec reporter
.NET SDK 10 Dotnet reporter (Microsoft Testing Platform)
Playwright deps Browser automation for Storybook test-runner
Claude Code AI assistance
Dev tools Git, zsh, fzf, Docker, gh, vim, nano, git-delta

Key files:

  • Dockerfile - Container definition
  • devcontainer.json - IDE configuration
  • scripts/ - Setup and configuration scripts
    • init-firewall.sh - Network security
    • setup-dev-environment.sh - Main setup orchestrator

Prerequisites

Choose one of:

Quick Start

VS Code

  1. Install Dev Containers extension
  2. Open project → Click green button (bottom-left) → "Reopen in Container"
  3. Wait for build → Start coding

Learn more about VS Code Dev Containers →

IntelliJ IDEA

  1. File → Remote Development → Dev Containers → New Dev Container → From Local Project
  2. Select Docker/Colima connection
  3. Browse to /.devcontainer/devcontainer.json
  4. Build Container and Continue

Resources:

Tips & Troubleshooting

Colima Setup

IntelliJ requires more resources than the default:

bash
# Basic setup
colima start --cpu 2 --memory 4

# With performance optimization
colima start --cpu 4 --memory 8 --vm-type vz --mount-type virtiofs

macOS Performance

For better file performance:

  • Docker Desktop: Enable VirtioFS in Settings → General → Choose file sharing implementation
  • Colima: Use --vm-type vz --mount-type virtiofs flag when starting (see above)

Common Commands

bash
npm test               # Run all tests
npm run test:unit      # Run unit tests only
npm run test:reporters # Run reporter tests
npm run checks         # Run all quality checks (typecheck, lint, format, test)

Additional Notes

  • Command history is preserved between container sessions in a persistent volume
  • Network isolation restricts outbound connections for security (see scripts/init-firewall.sh for allowed services)
  • Environment variables like NODE_OPTIONS and USE_SYSTEM_CLAUDE are pre-configured
  • Timezone defaults to Europe/Stockholm but can be changed via TZ build arg

Overview (`plugin/README.md`)

TDD Guard Plugin

Automated Test-Driven Development enforcement for Claude Code.

When Claude Code attempts to edit or write files, TDD Guard validates that changes follow TDD principles — blocking operations that skip tests or over-implement.

Setup

After installing the plugin, run the setup skill to configure a test reporter for your project:

text
/tdd-guard:setup

This detects your test framework and configures the appropriate reporter.

Note: You may need to restart your terminal session or IDE extension for the setup skill to appear.

What's included

  • Hooks — Automatically registered on install. Validate file operations, handle tdd-guard on/off commands, and manage session state.
  • Setup skill — /tdd-guard:setup detects your test framework and configures the reporter.

Requirements

  • Node.js 22+
  • A supported test framework

Learn more


Overview (`reporters/dotnet/README.md`)

TDD Guard .NET Reporter

Microsoft Testing Platform (MTP) V2 extension that captures test results
for TDD Guard validation. Works with any .NET test framework that supports
MTP V2 — one package covers TUnit, MSTest, xUnit, NUnit, and more.

How it works

The reporter is an MTP V2 extension that loads in-process alongside your
test framework. It subscribes to TestNodeUpdateMessage events — the
standard MTP event type that all frameworks publish when a test completes.

When a test session finishes, the extension writes results to
.claude/tdd-guard/data/test.json in the TDD Guard wire format. The
TDD Guard validation hook reads this file to enforce TDD discipline.

The extension is framework-agnostic. It does not know or care which
framework is running. MTP handles the framework integration; we handle
the test result capture.

Requirements

Installation

Planned distribution: NuGet.org (published by the maintainer).

Add a PackageReference to your test project:

xml
<ItemGroup>
  <PackageReference Include="TddGuard.Dotnet" Version="*" />
</ItemGroup>

For solutions with multiple test projects, add it once via
Directory.Build.props in your tests directory:

xml
<Project>
  <ItemGroup>
    <PackageReference Include="TddGuard.Dotnet" Version="*" />
  </ItemGroup>
</Project>

No manual hook code is needed — the package auto-registers via
buildTransitive/*.props which injects the MTP builder hook at build time.

Usage

The extension registers automatically. Running dotnet test is enough
once the package is referenced.

Configuration

Project Root

Set the TDD_GUARD_PROJECT_ROOT environment variable to your project root:

bash
export TDD_GUARD_PROJECT_ROOT="/absolute/path/to/project/root"

CLAUDE_PROJECT_DIR is used as a fallback when TDD_GUARD_PROJECT_ROOT
is not set. Claude Code sets this variable for hook commands, though it
may not be available in all execution contexts.

Rules

  • Absolute and relative paths are both accepted (per ADR-009)
  • Current directory must be within the configured project root
  • When neither env var is set, the extension logs a diagnostic to stderr
    and disables itself (per ADR-010) — it does not silently fall back to cwd

Compatibility

Verified with smoke tests in TddGuard.Dotnet.Compat.* projects:

Framework MTP V2 support Status Notes
TUnit Native Verified File path as module ID
MSTest v4 Native Verified File path as module ID
xUnit v3 Native Verified Via xunit.v3.mtp-v2 package
xUnit v2 Via adapter Verified Via YTest.MTP.XUnit2 (third-party, unofficial)
NUnit 4 Via runner Verified UID as module ID (NUnit omits file paths)
Reqnroll Inherited Expected BDD layer over MSTest/NUnit/xUnit/TUnit

Project structure

text
TddGuard.Dotnet.Core/       Domain types and pure functions (no MTP dependency)
TddGuard.Dotnet/             MTP V2 extension (listener, builder, hook)
TddGuard.Dotnet.Tests/       Unit tests, property-based tests, test infrastructure
TddGuard.Dotnet.Compat.*/    Framework compatibility smoke tests

Core has no dependency on MTP. It defines the domain types
(TestState, CollectedResult, TestRunOutput), the serializer,
the file writer, and the project root resolver. Dotnet depends on
Core and MTP, wiring the domain logic into the MTP extension model.

This separation means the domain logic can be tested without MTP,
and the MTP integration can change without affecting the domain.

Development

All commands run inside the devcontainer:

bash
docker exec -w /workspace/reporters/dotnet <container> dotnet build
docker exec -w /workspace/reporters/dotnet <container> dotnet test --project TddGuard.Dotnet.Tests

Find the container name via docker ps.

See TddGuard.Dotnet.Tests/README.md for testing conventions,
infrastructure, and the framework smoke test guide.

License

MIT


Overview (`reporters/go/README.md`)

TDD Guard Go Reporter

Go test reporter that captures test results for TDD Guard validation.

Requirements

Installation

bash
go install github.com/nizos/tdd-guard/reporters/go/cmd/tdd-guard-go@latest

Configuration

Basic Usage

Pipe go test -json output to the reporter:

bash
go test -json ./... 2>&1 | tdd-guard-go

Project Root Configuration

For projects where tests run in subdirectories, specify the project root:

bash
go test -json ./... 2>&1 | tdd-guard-go -project-root /absolute/path/to/project/root

Configuration Rules

  • Path must be absolute when using -project-root flag
  • Current directory must be within the configured project root
  • Falls back to current directory if not specified

Makefile Integration

Add to your Makefile:

makefile
test:
	go test -json ./... 2>&1 | tdd-guard-go -project-root /absolute/path/to/project/root

How It Works

The reporter acts as a filter that:

  1. Reads go test -json output from stdin
  2. Passes the output through to stdout unchanged
  3. Parses test results and transforms them to TDD Guard format
  4. Saves results to .claude/tdd-guard/data/test.json

This design allows it to be inserted into existing test pipelines without disrupting output.

More Information

License

MIT


Overview (`reporters/jest/README.md`)

TDD Guard Jest Reporter

Jest reporter that captures test results for TDD Guard validation.

Requirements

  • Node.js 18+
  • Jest 30.0.5+
  • TDD Guard installed globally

Installation

bash
npm install --save-dev tdd-guard-jest

Configuration

Jest Configuration

Add the reporter to your jest.config.js:

javascript
module.exports = {
  reporters: [
    'default',
    [
      'tdd-guard-jest',
      {
        projectRoot: __dirname,
      },
    ],
  ],
}

Or in jest.config.ts:

typescript
import type { Config } from 'jest'
import path from 'path'

const config: Config = {
  reporters: [
    'default',
    [
      'tdd-guard-jest',
      {
        projectRoot: path.resolve(__dirname),
      },
    ],
  ],
}

export default config

Workspace/Monorepo Configuration

For workspaces or monorepos, pass the project root path to the reporter:

javascript
// jest.config.js in project root
const path = require('path')

module.exports = {
  reporters: [
    'default',
    [
      'tdd-guard-jest',
      {
        projectRoot: path.resolve(__dirname),
      },
    ],
  ],
}

If your jest config is in a workspace subdirectory, pass the absolute path to your project root:

javascript
module.exports = {
  reporters: [
    'default',
    [
      'tdd-guard-jest',
      {
        projectRoot: '/Users/username/projects/my-app',
      },
    ],
  ],
}

More Information

License

MIT


Overview (`reporters/junit5/README.md`)

TDD Guard JUnit5 Reporter

JUnit5 TestExecutionListener that captures test results for TDD Guard validation.

Requirements

Installation

Planned distribution: Maven Central (published by the maintainer).

Gradle:

kotlin
testImplementation("io.github.nizos:tdd-guard-junit5:0.1.0")

Maven:

xml
<dependency>
    <groupId>io.github.nizos</groupId>
    <artifactId>tdd-guard-junit5</artifactId>
    <version>0.1.0</version>
    <scope>test</scope>
</dependency>

Usage

The listener registers automatically via the JUnit Platform SPI. Running ./gradlew test or mvn test is enough once the dependency is on the test classpath.

Configuration

Project Root Configuration

Set the TDD_GUARD_PROJECT_ROOT environment variable to your project root:

bash
export TDD_GUARD_PROJECT_ROOT="/absolute/path/to/project/root"

Or use a relative path, which resolves against the test runner's cwd.

Configuration Rules

  • Absolute and relative paths are both accepted (per ADR-009)
  • Current directory must be within the configured project root
  • Errors when neither an explicit path nor the env var is configured (per ADR-010)

Development

bash
./gradlew test

More Information

License

MIT


Overview (`reporters/minitest/README.md`)

TDD Guard Minitest Reporter

Minitest reporter that captures test results for TDD Guard validation.

Requirements

Installation

Install TDD Guard by following the instructions in the TDD Guard repository.

Add the reporter to your Gemfile:

ruby
gem "tdd-guard-minitest"

Or install directly:

bash
gem install tdd-guard-minitest

Usage

Require the autorun entry point before your test files. This registers the reporter as a Minitest plugin and also installs an at_exit hook that captures load errors (for example, a new test file that requires an implementation file that doesn't exist yet) as synthetic failed tests, so TDD Guard can see that a test tried to run.

Run a single test file directly:

bash
ruby -rtdd_guard_minitest/autorun test/my_test.rb

For Rails or Rake projects, require it from test/test_helper.rb:

ruby
require "tdd_guard_minitest/autorun"

Or pass it to your Rake::TestTask so every rake test invocation loads it first:

ruby
Rake::TestTask.new do |t|
  t.ruby_opts << "-rtdd_guard_minitest/autorun"
end

Configuration

Project Root Configuration

Set the TDD_GUARD_PROJECT_ROOT environment variable to your project root:

bash
export TDD_GUARD_PROJECT_ROOT="/absolute/path/to/project/root"

Configuration Rules

  • Set TDD_GUARD_PROJECT_ROOT to the project root. Absolute and relative paths are both accepted; relative paths resolve against the working directory at runtime.
  • Tests must be run from a directory at or below the project root.
  • The reporter raises at startup when the variable is not set. It no longer silently falls back to the current directory.

Development

bash
bundle install
bundle exec rspec

More Information

License

MIT


Overview (`reporters/phpunit/README.md`)

TDD Guard PHPUnit Reporter

PHPUnit reporter that captures test results for TDD Guard validation.

Requirements

  • PHP 8.1+
  • PHPUnit 9.0+ or 10.0+ or 11.0+ or 12.0+
  • TDD Guard installed globally

Installation

bash
composer require --dev tdd-guard/phpunit

Configuration

PHPUnit 10+ Configuration

Add the extension to your phpunit.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
         bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    
    <extensions>
        <bootstrap class="TddGuard\PHPUnit\TddGuardExtension">
            <parameter name="projectRoot" value="/absolute/path/to/project/root"/>
        </bootstrap>
    </extensions>
</phpunit>

PHPUnit 9.x Configuration

Add the listener to your phpunit.xml:

xml
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory>tests</directory>
        </testsuite>
    </testsuites>
    
    <listeners>
        <listener class="TddGuard\PHPUnit\TddGuardListener">
            <arguments>
                <string>/absolute/path/to/project/root</string>
            </arguments>
        </listener>
    </listeners>
</phpunit>

Project Root Configuration

Set the project root using any ONE of these methods:

Option 1: PHPUnit Configuration (Recommended)

Use the projectRoot parameter in your phpunit.xml (see examples above).

Option 2: Environment Variable

bash
export TDD_GUARD_PROJECT_ROOT=/absolute/path/to/project/root

Configuration Rules

  • Project root must be configured via the projectRoot parameter or TDD_GUARD_PROJECT_ROOT environment variable
  • Tests must be run from somewhere within the project root directory
  • Relative paths are supported but resolve against the working directory at runtime — if tests may run from different directories, use an absolute path to ensure results are always written to the correct location

More Information

License

MIT


Overview (`reporters/phpunit/SYNC_README.md`)

TDD Guard PHPUnit Reporter

This repository is automatically synchronized from the main TDD Guard monorepo.

Important Notice

This is a read-only mirror. Please do not submit pull requests here.

Installation

bash
composer require --dev tdd-guard/phpunit

Why a Separate Repository?

Packagist requires composer.json to be at the root of the repository. Since TDD Guard is a monorepo containing multiple packages (npm, Python, PHP), we maintain this synchronized copy specifically for Packagist distribution.

Synchronization

This repository is automatically updated whenever changes are pushed to the reporters/phpunit directory in the main repository.

License

MIT - See the main repository for details.


Overview (`reporters/pytest/README.md`)

TDD Guard Pytest Reporter

Pytest plugin that captures test results for TDD Guard validation.

Requirements

  • Python 3.8+
  • pytest 6.0+
  • TDD Guard installed globally

Installation

bash
pip install tdd-guard-pytest

The plugin activates automatically when installed.

Configuration

Project Root Configuration

Set tdd_guard_project_root to your project root using any ONE of these methods:

Option 1: pyproject.toml

toml
[tool.pytest.ini_options]
tdd_guard_project_root = "/absolute/path/to/project/root"

Option 2: pytest.ini

ini
[pytest]
tdd_guard_project_root = /absolute/path/to/project/root

Option 3: setup.cfg

ini
[tool:pytest]
tdd_guard_project_root = /absolute/path/to/project/root

Configuration Rules

  • Path must be absolute
  • Current directory must be within the configured project root
  • Falls back to current directory if configuration is invalid

Development

When developing the pytest reporter, you need to configure the project root to ensure test results are saved to the correct location:

  1. Copy the example configuration:

    bash
    cp pytest.ini.example pytest.ini
  2. Edit pytest.ini and set the absolute path to your TDD Guard project root:

    ini
    [pytest]
    tdd_guard_project_root = /absolute/path/to/tdd-guard

Note: pytest.ini is gitignored to avoid committing machine-specific paths.

More Information

License

MIT


Overview (`reporters/rspec/README.md`)

TDD Guard RSpec Reporter

RSpec formatter that captures test results for TDD Guard validation.

Requirements

Installation

Install TDD Guard by following the instructions in the TDD Guard repository.

Add the reporter to your Gemfile:

ruby
gem "tdd-guard-rspec"

Or install directly:

bash
gem install tdd-guard-rspec

Usage

Run RSpec with the TDD Guard formatter:

bash
rspec --format TddGuardRspec::Formatter

Or add it to your .rspec file:

text
--format TddGuardRspec::Formatter

Configuration

Project Root Configuration

Set the TDD_GUARD_PROJECT_ROOT environment variable to your project root:

bash
export TDD_GUARD_PROJECT_ROOT="/absolute/path/to/project/root"

Configuration Rules

  • Set TDD_GUARD_PROJECT_ROOT to the project root. Absolute and relative paths are both accepted; relative paths resolve against the working directory at runtime.
  • Tests must be run from a directory at or below the project root.
  • The reporter raises at startup when the variable is not set. It no longer silently falls back to the current directory.

Development

bash
bundle install
bundle exec rspec

More Information

License

MIT


Overview (`reporters/rust/README.md`)

TDD Guard Rust Reporter

Rust test reporter that captures test results for TDD Guard validation.

Note: This reporter is part of the TDD Guard project, which ensures Claude Code follows Test-Driven Development principles.

Requirements

  • Rust 1.70+
  • TDD Guard installed globally
  • cargo-nextest (recommended) or cargo test with JSON output support

Installation

Step 1: Install TDD Guard

bash
npm install tdd-guard

Step 2: Install the Rust reporter

bash
cargo install tdd-guard-rust

Usage

The reporter works as a filter that processes test output while passing it through unchanged.

bash
cargo nextest run 2>&1 | tdd-guard-rust --project-root /absolute/path/to/project

With cargo test

bash
cargo test -- -Z unstable-options --format json 2>&1 | tdd-guard-rust --project-root /absolute/path/to/project

Direct Execution

The reporter can also execute tests directly:

bash
# Auto-detect runner (prefers nextest)
tdd-guard-rust --project-root /absolute/path/to/project

# Force specific runner
tdd-guard-rust --project-root /absolute/path/to/project --runner nextest
tdd-guard-rust --project-root /absolute/path/to/project --runner cargo

Makefile Integration

Add to your Makefile:

makefile
.PHONY: test
test:
	cargo nextest run 2>&1 | tdd-guard-rust --project-root $(PWD) --passthrough

.PHONY: test-tdd
test-tdd:
	tdd-guard on && $(MAKE) test

Configuration

Project Root

The --project-root flag must be an absolute path to your project directory. This is where the .claude/tdd-guard/data/test.json file will be written.

Flags

  • --passthrough: Force passthrough mode even if stdin is a terminal
  • --runner [auto|nextest|cargo]: Choose test runner for direct execution (default: auto)
  • --project-root: Absolute path to project directory (required)

How It Works

The reporter captures JSON-formatted test output, passes it through unchanged to stdout, and saves TDD Guard-formatted results to .claude/tdd-guard/data/test.json.

License

MIT - See LICENSE file in the repository root.


Overview (`reporters/storybook/README.md`)

TDD Guard Storybook Reporter

Storybook test-runner reporter that captures test results for TDD Guard validation.

Requirements

  • Node.js 18+
  • Storybook 7.x - 9.x
  • @storybook/test-runner 0.19.0+
  • TDD Guard installed globally

[!IMPORTANT]
For Storybook v10.x, use the tdd-guard-vitest reporter w/ Vitest

Installation

bash
npm install --save-dev tdd-guard-storybook

Configuration

Basic Configuration

Add the reporter to your .storybook/test-runner.ts:

typescript
import { StorybookReporter } from 'tdd-guard-storybook'

const reporter = new StorybookReporter()

module.exports = {
  async postVisit(page, context) {
    await reporter.onStoryResult(context)
  },
}

process.on('exit', () => {
  reporter.onComplete()
})

Workspace/Monorepo Configuration

For workspaces or monorepos, pass the project root path in the reporter options:

typescript
import { StorybookReporter } from 'tdd-guard-storybook'
import path from 'path'

const reporter = new StorybookReporter({
  projectRoot: path.resolve(__dirname, '../..'),
})

module.exports = {
  async postVisit(page, context) {
    await reporter.onStoryResult(context)
  },
}

process.on('exit', () => {
  reporter.onComplete()
})

If your test-runner config is in a subdirectory, specify the absolute path to your project root in the options, for example { projectRoot: '/Users/username/projects/my-app' }.

How It Works

The reporter captures results from stories that have play functions (interaction tests):

  • Stories with play functions are reported as tests
  • Stories without play functions are ignored
  • Test results are saved to .claude/tdd-guard/data/test.json

Output Format

The reporter writes test results in a format compatible with TDD Guard validation:

json
{
  "testModules": [
    {
      "moduleId": "button--primary",
      "tests": [
        {
          "name": "Primary",
          "fullName": "Button > Primary",
          "state": "passed"
        }
      ]
    }
  ],
  "unhandledErrors": [],
  "reason": "passed"
}

More Information

License

MIT


Overview (`reporters/vitest/README.md`)

TDD Guard Vitest Reporter

Vitest reporter that captures test results for TDD Guard validation.

Requirements

  • Node.js 18+
  • Vitest 3.2.0+
  • TDD Guard installed globally

Installation

bash
npm install --save-dev tdd-guard-vitest

Configuration

Vitest Configuration

Add the reporter to your vitest.config.ts:

typescript
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    reporters: ['default', ['tdd-guard-vitest']],
  },
})

Workspace/Monorepo Configuration

For workspaces or monorepos, pass the project root path in the reporter options:

typescript
// vitest.config.ts in project root
import { defineConfig } from 'vitest/config'
import path from 'path'

export default defineConfig({
  test: {
    reporters: [
      'default',
      ['tdd-guard-vitest', { projectRoot: path.resolve(__dirname) }],
    ],
  },
})

If your vitest config is in a workspace subdirectory, specify the absolute path to your project root in the options, for example { projectRoot: '/Users/username/projects/my-app' }.

More Information

License

MIT


Overview (`reporters/dotnet/TddGuard.Dotnet.Tests/README.md`)

TddGuard.Dotnet.Tests

Testing philosophy

Tests verify behaviour, not implementation. The system under test (SUT) is
TddGuardListener exercised through its MTP interface — events go in, JSON
comes out. Tests never reference internal types like CollectedResult,
TestRunSummariser, or TestReportSerializer directly.

This means we can refactor internals freely without breaking tests, and every
test failure points to a genuine change in observable behaviour.

Test layers

Unit tests (`TddGuardListenerTests`)

Example-based tests that verify specific, named behaviours: "writes 'passed'
reason for passing test", "ignores in-progress nodes", etc. Each test follows
strict Arrange/Act/Assert via the ListenerFixture DSL:

csharp
await ListenerFixture
    .Arrange(async listener =>
    {
        await listener.ConsumeAsync(StubProducer(), An.Event().Named("test1").Passed(), default);
    })
    .Act()
    .Assert(async root =>
    {
        await Assert.That(root.Reason()).IsEqualTo("passed");
    });
  • Arrange feeds MTP events to the listener
  • Act triggers OnTestSessionFinishingAsync (the write)
  • Assert inspects the resulting JSON

These tests are fast, deterministic, and produce pinpoint failure messages.

Property-based tests (`TddGuardListenerPropertyTests`)

FsCheck-driven tests that verify universal properties across hundreds of
randomly generated event sequences: "count is preserved", "reason reflects
failures", "output is valid JSON", etc.

Properties use ListenerFixture.Execute() which returns a synchronous
PipelineResult suitable for FsCheck's Prop.ForAll:

csharp
Prop.ForAll(Arb.From(genEvents), events =>
{
    using var result = RunEvents(events);
    return result.Output!.Value.GetProperty("reason").GetString() == expected;
});

These tests explore the input space far more broadly than hand-crafted examples
and catch edge cases a person would never consider.

Supporting test files

File Purpose
ProjectRootResolverTests Unit tests for env var resolution (pure function, tested directly)
ReportFileWriterTests Unit tests for file I/O (temp dir, error paths, overwrite)
DiagnosticDecoratorTests Unit tests for logging decorators (spy-based)
TestReportWriterTests Unit tests for the write-or-skip orchestrator
TestNodeMapperTests Unit tests for UID parsing (pure function)
TddGuardBuilderTests Unit tests for the MTP registration composition root

These test lower-level components directly because their contracts are stable
public APIs (file paths, env vars, JSON output). They don't test internal
wiring — that's covered by the outside-in listener tests and PBTs.

Test infrastructure

`ListenerFixture`

The test harness that manages the listener lifecycle. Provides three APIs:

  • Arrange(...).Act().Assert(...) — async DSL for unit tests
  • Arrange(...).Act().AssertNoOutput() — for tests expecting no JSON output
  • Arrange(...).Act().Execute() — synchronous pipeline for PBTs, returns PipelineResult
  • Create() — manual session control for tests that need multiple sessions or concurrency

`TestEventBuilder` (`An.Event()`)

Test Data Builder for MTP events. Hides MTP-specific types behind
intent-revealing methods:

csharp
An.Event().Named("test1").InFile("/src/A.cs").Passed()
An.Event().Named("test1").Failed("assertion message")
An.Event().Named("test1").FailedWithExplanation("no exception, just text")
An.Event().Named("test1").FailedBare()       // no exception, no explanation
An.Event().Named("test1").Error("setup failed")
An.Event().Named("test1").Skipped()
An.Event().Named("test1").InProgress()
An.Event().Named("test1").Discovered()
An.Event().Named("test1").WithNoFilePath().Passed()  // null file path

`MtpStubs`

Low-level MTP stubs and the FsCheck generator (GenTaggedEvent). The generator
produces TaggedEvent records that pair an MTP message with a bool IsFailed
flag, so PBT properties can compute expected outcomes without inspecting MTP
internals.

`TestJsonAssert`

Extension methods for navigating JsonElement in assertions: root.Reason(),
root.Module().Test().State(), root.Module().Test().ErrorMessage(), etc.

`TempDir`

Disposable temp directory helper for file I/O tests.

Framework compatibility smoke tests

The TddGuard.Dotnet.Compat.* projects sit alongside the test project
and prove the MTP extension works with every major .NET test framework.
Each is a standalone runnable test project with a single passing test.

Project Framework Notes
Compat.TUnit TUnit 0.x Native MTP, file path as module ID
Compat.MSTest MSTest v4 Native MTP, file path as module ID
Compat.XUnit xUnit v3 Native MTP via xunit.v3.mtp-v2, file path as module ID
Compat.XUnit2 xUnit v2 Third-party adapter (YTest.MTP.XUnit2), hash as module ID
Compat.NUnit NUnit 4 MTP via NUnit runner, UID as module ID (no file path)

How they work

Each project has three files:

  • .csproj targets net10.0, references the framework package and
    TddGuard.Dotnet via ProjectReference. A <TestingPlatformBuilderHook>
    MSBuild item tells MTP's codegen where to find the hook class.
  • TestingPlatformBuilderHook.cs is the MTP entry point. MTP's
    source generator calls AddExtensions at startup, which delegates to
    TddGuardBuilder.Register(builder) to wire up the listener.
  • SmokeTests.cs has one passing test in the framework's native syntax.

When dotnet test runs, MTP starts the test host, calls the hook,
registers our listener, the framework discovers and runs the test,
MTP publishes TestNodeUpdateMessage events, our listener collects
them, and OnTestSessionFinishingAsync writes test.json.

The extension never knows which framework is running. It subscribes to
TestNodeUpdateMessage which is a generic MTP event type. The only
observable differences between frameworks are in module ID source
(file path vs UID fallback) and UID format, both handled by existing
null-checks in TestNodeMapper.

Why ProjectReference instead of NuGet

The compat projects reference TddGuard.Dotnet via ProjectReference
(local source) rather than PackageReference (NuGet). This avoids the
NuGet pack/restore cycle and firewall issues with CDN IPs. The manual
<TestingPlatformBuilderHook> MSBuild item replaces what the NuGet
package's buildTransitive props would normally inject. The integration
tests (reporters/test/factories/dotnet.ts) separately test the NuGet
packaging path.

Running the smoke tests

bash
export TDD_GUARD_PROJECT_ROOT=/workspace
cd /workspace/reporters/dotnet/TddGuard.Dotnet.Compat.MSTest
dotnet test
cat /workspace/.claude/tdd-guard/data/test.json

Repeat for each Compat.* project. If test.json appears with
"reason":"passed", the framework works.

Why outside-in

Tests are written from the perspective of the consumer, not the
implementer. The SUT (system under test) is the TddGuardListener
exercised through its MTP interface: feed it events, trigger the
session finish, read the JSON.

This matters because:

  • Refactoring is free. You can restructure Summarise, change how
    Serialize works, rename internal types, or merge classes — and no
    test breaks as long as the JSON output stays the same.
  • Tests document behaviour. Each test says what the system does
    ("writes 'passed' reason for passing test"), not how it does it.
    New contributors read the tests to understand the contract.
  • Agents continue the pattern. If you are an AI agent adding tests:
    use ListenerFixture, use An.Event() to build inputs, assert on
    the JSON output. Do not reference CollectedResult, TestRunOutput,
    Summarise, or Serialize in test assertions. Those are internals.

The supporting tests (ProjectRootResolverTests, ReportFileWriterTests,
etc.) test lower-level components directly because their contracts are
stable public boundaries (env vars, file paths, JSON structure). They
follow the same principle: test through the public API, not the internals.

Running tests

All commands run inside the devcontainer:

bash
docker exec -w /workspace/reporters/dotnet <container> dotnet test --project TddGuard.Dotnet.Tests

For coverage:

bash
docker exec -w /workspace/reporters/dotnet <container> dotnet test --project TddGuard.Dotnet.Tests \
  -- --coverage --coverage-output-format cobertura --coverage-output /tmp/coverage.xml

Config Migration (`docs/config-migration.md`)

Configuration Migration Guide

This guide helps you migrate from the legacy MODEL_TYPE configuration to the new VALIDATION_CLIENT system.

What Changed

The old configuration system required:

  • MODEL_TYPE to choose between claude_cli or anthropic_api
  • USE_SYSTEM_CLAUDE (true/false) to choose between system or local Claude binary
  • Complex setup for finding and configuring the Claude binary

The new system simplifies this with:

  • VALIDATION_CLIENT to choose between sdk, api, or cli (deprecated)
  • No binary configuration needed for SDK (the default)

Migration Instructions

From Claude CLI to SDK

Old configuration:

bash
MODEL_TYPE=claude_cli
USE_SYSTEM_CLAUDE=true  # or false

New configuration:

bash
VALIDATION_CLIENT=sdk  # Or omit entirely, as SDK is the default

The SDK client eliminates the need for:

  • Finding the Claude binary location
  • Setting USE_SYSTEM_CLAUDE
  • Dealing with symlinks or PATH configuration

From Anthropic API

Old configuration:

bash
MODEL_TYPE=anthropic_api
TDD_GUARD_ANTHROPIC_API_KEY=your-api-key-here

New configuration:

bash
VALIDATION_CLIENT=api
TDD_GUARD_ANTHROPIC_API_KEY=your-api-key-here  # Same key variable

The API configuration remains similar, just with a clearer variable name.

Legacy CLI Client (Deprecated)

If you must continue using the CLI client (not recommended):

bash
VALIDATION_CLIENT=cli  # Deprecated - use sdk instead

You'll still need to configure the Claude binary location as described in the CLI Binary Configuration section below.

Why CLI is Deprecated

The SDK client is easier to work with and requires less configuration for different setups and operating systems.

Deprecated Variables

Variable Replacement Notes
MODEL_TYPE VALIDATION_CLIENT Map claude_cli → sdk, anthropic_api → api
USE_SYSTEM_CLAUDE None No longer needed with SDK
TEST_MODEL_TYPE None Use consistent configuration

Common Migration Issues

API Key Conflicts

For information about API key conflicts and billing, see the Validdation Model Configuration documentation.

CLI Binary Configuration

If you're still using the deprecated CLI client (VALIDATION_CLIENT=cli), you need to help TDD Guard find your Claude installation.

Finding Your Claude Installation

bash
# Check system-wide installation
which claude

# Check local installation
ls ~/.claude/local/claude

Configuration Options

Option 1: Environment Variable

If Claude is in your PATH:

bash
USE_SYSTEM_CLAUDE=true

Option 2: Symlink

Point to your Claude installation:

bash
# Create directory if needed
mkdir -p ~/.claude/local

# Create symlink to your Claude binary
ln -s /path/to/your/claude ~/.claude/local/claude

Example for Homebrew on macOS:

bash
ln -s /opt/homebrew/bin/claude ~/.claude/local/claude

Option 3: Migrate Installation

Use Claude Code's built-in command:

bash
/migrate-installer

Getting Help

If you encounter issues during migration:

  1. Check the main configuration guide
  2. Review the Validdation Model Configuration
  3. Open an issue at github.com/nizos/tdd-guard/issues

Configuration (`docs/configuration.md`)

Configuration Guide

This guide covers the configuration options for TDD Guard.

Environment Variables

TDD Guard uses environment variables for configuration.
Create a .env file in your project root:

Note: If you're migrating from an older version using MODEL_TYPE, see the Configuration Migration Guide.

bash
# Validation client for TDD enforcement (optional)
# Options: 'sdk' (default) or 'api'
VALIDATION_CLIENT=sdk

# Model version for validation (optional)
# Default: claude-sonnet-4-6
# See https://docs.anthropic.com/en/docs/about-claude/models/overview
TDD_GUARD_MODEL_VERSION=claude-sonnet-4-6

# Anthropic API Key
# Required when VALIDATION_CLIENT is set to 'api'
# Get your API key from https://console.anthropic.com/
TDD_GUARD_ANTHROPIC_API_KEY=your-api-key-here

# Linter type for refactoring phase support (optional)
# Options: 'eslint', 'golangci-lint' or unset (no linting)
# See docs/linting.md for detailed setup and configuration
LINTER_TYPE=eslint

Model Configuration

TDD Guard supports multiple validation clients:

  • SDK (default) - Uses your Claude Code subscription
  • API - Separate billing for CI/CD or faster validation
  • CLI (deprecated) - Legacy option, not recommended

For detailed configuration, billing information, and troubleshooting, see the Validation Model Configuration guide.

If you're using the deprecated CLI client, see the Configuration Migration Guide.

Settings File Locations

Choose where to save your settings based on your needs:

  • Project settings (.claude/settings.json) - Recommended for team consistency
  • Local settings (.claude/settings.local.json) - For personal preferences
  • User settings (~/.claude/settings.json) - For global configuration

Hook Configuration

If you installed TDD Guard as a plugin, hooks are configured automatically. For manual hook setup, see the installation guide.

Related:

Test Reporter Configuration

Custom Validation Rules

See Custom Instructions to customize TDD validation rules to match your practices.

Data Storage

TDD Guard stores context data in .claude/tdd-guard/data/:

  • instructions.md - Your custom TDD validation rules (created automatically, never overwritten)
  • test.json - Latest test results from your test runner (Vitest or pytest)
  • todos.json - Current todo state
  • modifications.json - File modification history
  • lint.json - ESLint results (only created when LINTER_TYPE=eslint)

This directory is created automatically and should be added to .gitignore.

Troubleshooting

Dependency Versions

Vitest

Use the latest Vitest version to ensure correct test output format for TDD Guard:

bash
npm install --save-dev vitest@latest

pytest

For Python projects, ensure you have a recent version of pytest:

bash
pip install pytest>=7.0.0

Common Issues

  1. TDD Guard not triggering: Check that hooks are properly configured in .claude/settings.json
  2. Test results not captured: Ensure VitestReporter is added to your Vitest config
  3. "Command not found" errors: Make sure tdd-guard is installed globally with npm install tdd-guard
  4. Changes not taking effect: Restart your Claude session after modifying hooks or environment variables

Updating TDD Guard

To update to the latest version:

bash
# Update CLI tool
npm update -g tdd-guard

# For JavaScript/TypeScript projects, update the Vitest reporter in your project
npm update tdd-guard-vitest

# For Python projects, update the pytest reporter
pip install --upgrade tdd-guard-pytest

Check your current version:

bash
npm list -g tdd-guard
pip show tdd-guard-pytest

Custom Instructions (`docs/custom-instructions.md`)

Custom TDD Instructions

Customize TDD Guard's validation rules to match your specific TDD practices.

How It Works

TDD Guard uses validation rules to enforce TDD principles. You can override these default rules by creating a custom instructions file at .claude/tdd-guard/data/instructions.md.

Automatic Setup

If you have the SessionStart hook configured, the instructions file is created automatically with default rules when:

  • Starting a new Claude Code session
  • Resuming a session
  • Using the /clear command

Your custom instructions are never overwritten - once created, the file remains under your control.

Creating Custom Instructions

  1. Edit .claude/tdd-guard/data/instructions.md
  2. Adjust or replace the default rules with your TDD requirements
  3. Changes take effect immediately - no restart needed

Updating to Latest Defaults

When updating TDD Guard, you may want the latest default instructions:

  1. Delete .claude/tdd-guard/data/instructions.md
  2. Trigger the SessionStart hook (start new session or use /clear)
  3. The latest defaults will be created automatically

Alternatively, you can manually copy the default rules from src/validation/prompts/rules.ts.

Protecting Your Instructions

Prevent agents from modifying your custom instructions by denying access to TDD Guard data. See Strengthening TDD Enforcement for details.

Tips

  • Start with the default instructions and modify incrementally
  • Keep rules clear and actionable for consistent validation
  • Share effective customizations with the TDD Guard community in GitHub Discussions

Enforcement (`docs/enforcement.md`)

Strengthening TDD Enforcement

Ensure consistent TDD validation by preventing agents from modifying guard settings or bypassing file operation hooks.

Protect Guard Settings

Prevent agents from accessing TDD Guard's configuration and state:

json
{
  "permissions": {
    "deny": ["Read(.claude/tdd-guard/**)"]
  }
}

This protects your custom instructions, guard state, and test results from unintended changes.

Block File Operation Bypass

If your settings allow shell commands without approval, agents can modify files without triggering TDD validation. Block these commands to maintain enforcement:

json
{
  "permissions": {
    "deny": [
      "Bash(echo:*)",
      "Bash(printf:*)",
      "Bash(sed:*)",
      "Bash(awk:*)",
      "Bash(perl:*)"
    ]
  }
}

Note: Only needed if you've configured auto-approval for shell commands. May limit some agent capabilities.

Where to Apply

Add these settings to your chosen settings file. See Settings File Locations to choose the appropriate location.

Need Help?


Ignore Patterns (`docs/ignore-patterns.md`)

Ignore Patterns Guide

Configure TDD Guard to skip validation for specific files using glob patterns.

Why Use Ignore Patterns?

Control exactly which files TDD Guard validates. Useful for monorepos, rapid prototyping, or when different parts of your codebase need different validation rules.

Default Ignore Patterns

By default, TDD Guard ignores files with these extensions:

  • *.md - Markdown documentation
  • *.txt - Text files
  • *.log - Log files
  • *.json - JSON configuration files
  • *.yml / *.yaml - YAML configuration files
  • *.xml - XML files
  • *.html - HTML files
  • *.css - Stylesheets
  • *.erb - Rails ERB templates
  • *.rst - reStructuredText documentation

Custom Ignore Patterns

You can configure custom ignore patterns by creating a config.json file in the TDD Guard data directory (.claude/tdd-guard/data/):

json
{
  "guardEnabled": true,
  "ignorePatterns": [
    "*.md",
    "*.css",
    "*.json",
    "*.yml",
    "**/*.generated.ts",
    "**/public/**",
    "*.config.*"
  ]
}

Note: Custom patterns replace the default patterns entirely. If you want to keep some defaults (like *.md or *.json), include them in your custom list.

Pattern Syntax

Patterns use minimatch syntax (similar to .gitignore):

  • *.ext - Match files with extension (e.g., *.md)
  • dir/** - Match all files in directory (e.g., dist/**)
  • **/*.ext - Match extension anywhere (e.g., **/*.test.ts)
  • *.{js,ts} - Match multiple extensions (e.g., *.{yml,yaml})
  • path/**/*.ext - Match in specific path (e.g., src/**/*.spec.js)

Managing Patterns

Viewing Current Patterns

To see which patterns are currently active, check your config.json file:

bash
cat .claude/tdd-guard/data/config.json

If no custom patterns are configured, the default patterns listed above are used.

Updating Patterns

  1. Create or edit .claude/tdd-guard/data/config.json
  2. Add your ignorePatterns array
  3. The changes take effect immediately

Testing Patterns

To verify your patterns work as expected:

  1. Edit a file that should be ignored
  2. TDD Guard should skip validation immediately

Summary

Ignore patterns provide the flexibility to apply TDD validation exactly where you want it in your codebase. Start with the defaults, then customize as your project's needs evolve.


Installation (`docs/installation.md`)

Manual Installation

If you installed TDD Guard as a plugin, you don't need to do any of this manually — the plugin configures hooks automatically and the /tdd-guard:setup skill handles reporter installation and configuration.

This document is a reference for manual setup or troubleshooting.

1. Install TDD Guard

Using npm:

bash
npm install tdd-guard

Or using Homebrew:

bash
brew install tdd-guard

2. Configure Claude Code Hooks

Add the following hooks to your settings file. See Settings File Locations for guidance on which file to use.

You can also configure hooks interactively by typing /hooks in Claude Code.

json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit|TodoWrite",
        "hooks": [
          {
            "type": "command",
            "command": "tdd-guard"
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "tdd-guard"
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "matcher": "startup|resume|clear",
        "hooks": [
          {
            "type": "command",
            "command": "tdd-guard"
          }
        ]
      }
    ]
  }
}

3. Add Test Reporter

TDD Guard needs to capture test results from your test runner. Choose your language below:

JavaScript/TypeScript

Choose your test runner:

Vitest

Install the tdd-guard-vitest reporter in your project:

bash
npm install --save-dev tdd-guard-vitest

Add to your vitest.config.ts:

typescript
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    reporters: [
      'default',
      ['tdd-guard-vitest', { projectRoot: '/Users/username/projects/my-app' }],
    ],
  },
})

Jest

Install the tdd-guard-jest reporter in your project:

bash
npm install --save-dev tdd-guard-jest

Add to your jest.config.ts:

typescript
import type { Config } from 'jest'

const config: Config = {
  reporters: [
    'default',
    [
      'tdd-guard-jest',
      {
        projectRoot: '/Users/username/projects/my-app',
      },
    ],
  ],
}

export default config

Note: For both Vitest and Jest, specify the project root path when your test config is not at the project root (e.g., in workspaces or monorepos). This ensures TDD Guard can find the test results. See the reporter configuration docs for more details:

Storybook

Install the tdd-guard-storybook reporter in your project:

bash
npm install --save-dev tdd-guard-storybook

Add to your .storybook/test-runner.ts:

typescript
import { StorybookReporter } from 'tdd-guard-storybook'
import path from 'path'

const reporter = new StorybookReporter({
  projectRoot: path.resolve(__dirname, '..'),
})

module.exports = {
  async postVisit(page, context) {
    await reporter.onStoryResult(context)
  },
}

process.on('exit', () => {
  reporter.onComplete()
})

Note: The reporter is constructed directly in your .storybook/test-runner.ts and wired into Storybook test-runner's postVisit hook. Specify the project root path when your Storybook config is not at the project root. See the Storybook reporter configuration for more details.

Tip: For Storybook 10+ with Vite-based frameworks, consider using @storybook/addon-vitest instead. This runs Storybook tests through Vitest, allowing you to use the tdd-guard-vitest reporter for faster test execution. See Storybook with Vitest Addon for setup instructions.

Python (pytest)

Install the tdd-guard-pytest reporter:

bash
pip install tdd-guard-pytest

Configure the project root in your pyproject.toml:

toml
[tool.pytest.ini_options]
tdd_guard_project_root = "/Users/username/projects/my-app"

Note: Specify the project root path when your tests run from a subdirectory or in a monorepo setup. This ensures TDD Guard can find the test results. See the pytest reporter configuration for alternative configuration methods (pytest.ini, setup.cfg).

PHP (PHPUnit)

Install the tdd-guard/phpunit reporter in your project:

bash
composer require --dev tdd-guard/phpunit

For PHPUnit 9.x, add to your phpunit.xml:

xml
<listeners>
    <listener class="TddGuard\PHPUnit\TddGuardListener">
        <arguments>
            <string>/Users/username/projects/my-app</string>
        </arguments>
    </listener>
</listeners>

For PHPUnit 10.x/11.x/12.x, add to your phpunit.xml:

xml
<extensions>
    <bootstrap class="TddGuard\PHPUnit\TddGuardExtension">
        <parameter name="projectRoot" value="/Users/username/projects/my-app"/>
    </bootstrap>
</extensions>

Note: Specify the project root path when your phpunit.xml is not at the project root (e.g., in subdirectories or monorepos). This ensures TDD Guard can find the test results. The reporter saves results to .claude/tdd-guard/data/test.json.

Go

Install the tdd-guard-go reporter:

bash
go install github.com/nizos/tdd-guard/reporters/go/cmd/tdd-guard-go@latest

Pipe go test -json output to the reporter:

bash
go test -json ./... 2>&1 | tdd-guard-go -project-root /Users/username/projects/my-app

For Makefile integration:

makefile
test:
	go test -json ./... 2>&1 | tdd-guard-go -project-root /Users/username/projects/my-app

Note: The reporter acts as a filter that passes test output through unchanged while capturing results for TDD Guard. See the Go reporter configuration for more details.

Rust

Install the tdd-guard-rust reporter:

bash
cargo install tdd-guard-rust

Use it to capture test results from cargo test or cargo nextest:

bash
# With nextest (recommended)
cargo nextest run 2>&1 | tdd-guard-rust --project-root /Users/username/projects/my-app --passthrough

# With cargo test
cargo test -- -Z unstable-options --format json 2>&1 | tdd-guard-rust --project-root /Users/username/projects/my-app --passthrough

For Makefile integration:

makefile
test:
	cargo nextest run 2>&1 | tdd-guard-rust --project-root $(PWD) --passthrough

Note: The reporter acts as a filter that passes test output through unchanged while capturing results for TDD Guard. See the Rust reporter configuration for more details.

Troubleshooting

Hooks not working

The recommended installation method is via the plugin, as
described in the README.

Non-plugin installations might be incompatible with your current Claude Code version, hooks will
fail silently or exit with errors.

If you have an existing Homebrew or npm installation, remove it and switch to the plugin
installation.


Linting (`docs/linting.md`)

Linting and Refactoring Support

TDD Guard can optionally check code quality during the refactoring phase (when tests are green).
The following linters are supported: ESLint (JavaScript/TypeScript), golangci-lint (Go), and RuboCop (Ruby).
When issues are detected, the coding agent will be prompted to fix them.

Why Use Refactoring Support?

During the TDD green phase, the coding agent may:

  • Clean up implementation code
  • Extract methods or constants
  • Improve naming
  • Remove duplication

The refactoring support helps by:

  • Running ESLint automatically after file modifications
  • Detecting code quality issues
  • Prompting the coding agent to fix any issues found

Setup

  1. Install a supported linter in your project:

    For JavaScript/TypeScript (ESLint):

    bash
    npm install --save-dev eslint@latest

    For Go (golangci-lint): see the golangci-lint installation guide.

    For Ruby (RuboCop):

    bash
    gem install rubocop
  2. Enable linting by setting the environment variable:

    bash
    LINTER_TYPE=eslint          # for ESLint
    LINTER_TYPE=golangci-lint   # for golangci-lint
    LINTER_TYPE=rubocop         # for RuboCop
  3. Configure the PostToolUse hook

    You can configure this hook either through the interactive /hooks command or by manually editing your settings file. See Settings File Locations to choose the appropriate location. Use the same location as your PreToolUse hook.

    1. Type /hooks in Claude Code
    2. Select PostToolUse - After tool execution
    3. Choose + Add new matcher...
    4. Enter: Write|Edit|MultiEdit
    5. Select + Add new hook...
    6. Enter command: tdd-guard
    7. Choose where to save

    Manual Configuration

    Add to your chosen settings file:

    json
    {
      "hooks": {
        "PostToolUse": [
          {
            "matcher": "Write|Edit|MultiEdit",
            "hooks": [
              {
                "type": "command",
                "command": "tdd-guard"
              }
            ]
          }
        ]
      }
    }

How It Works

When enabled:

  1. After any file modification (Edit, MultiEdit, Write)
  2. TDD Guard runs the configured linter on modified files
  3. If issues are found, the coding agent receives a notification
  4. The agent will then fix the identified issues

Without LINTER_TYPE set, TDD Guard skips all linting operations.

Tip: Configure ESLint with complexity rules (e.g., complexity, max-depth) and the SonarJS plugin to encourage meaningful refactoring.
These rules help identify code that could benefit from simplification during the green phase.

ESLint Configuration

For effective refactoring support, consider adding these rules to your .eslintrc.js:

javascript
module.exports = {
  rules: {
    complexity: ['warn', 10],
    'max-depth': ['warn', 4],
    'max-lines-per-function': ['warn', 50],
    'max-nested-callbacks': ['warn', 3],
    'max-params': ['warn', 4],
  },
}

Troubleshooting

ESLint Not Running

  1. Verify ESLint is installed: npm list eslint
  2. Check that LINTER_TYPE=eslint is set in your .env file
  3. Ensure the PostToolUse hook is configured
  4. Restart your Claude session after making changes

RuboCop Not Running

  1. Verify RuboCop is installed: rubocop --version
  2. Check that LINTER_TYPE=rubocop is set in your .env file
  3. Ensure the PostToolUse hook is configured
  4. Restart your Claude session after making changes

Quick Commands (`docs/quick-commands.md`)

TDD Guard Quick Commands

TDD Guard can be quickly enabled or disabled using simple commands in your Claude Code session.
This is particularly useful when you need to temporarily disable TDD enforcement during prototyping or exploration phases.

Usage

Simply type one of these commands in your Claude Code prompt:

  • tdd-guard on - Enables TDD Guard enforcement
  • tdd-guard off - Disables TDD Guard enforcement

The commands are case-insensitive, so TDD-Guard OFF, tdd-guard off, and Tdd-Guard Off all work the same way.

Setup

To enable the quick commands feature, you need to add the UserPromptSubmit hook to your Claude Code configuration.
You can set this up either through the interactive /hooks command or by manually editing your settings file. See Settings File Locations to choose the appropriate location.

  1. Type /hooks in Claude Code
  2. Select UserPromptSubmit - When the user submits a prompt
  3. Select + Add new hook...
  4. Enter command: tdd-guard
  5. Choose where to save

Manual Configuration (Alternative)

Add the following to your chosen settings file:

json
{
  "hooks": {
    "userpromptsubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "tdd-guard"
          }
        ]
      }
    ]
  }
}

Note: Your configuration file may already have other hooks configured.
Simply add the userpromptsubmit section to your existing hooks object.

Tip: To prevent agents from modifying the TDD Guard state, see Strengthening TDD Enforcement.


Session Management (`docs/session-management.md`)

TDD Guard Session Management

The SessionStart hook manages TDD Guard's session data and ensures a clean slate for each Claude Code session.

What It Does

Clears Transient Data

  • Test results from previous sessions
  • Lint reports and code quality checks
  • Other temporary validation data

Sets Up Validation Rules

  • Creates the customizable instructions file if it doesn't exist
  • Preserves your custom rules if already configured
  • See Custom Instructions for details

Note: The guard's enabled/disabled state is preserved across sessions.

Setup

To enable session management, you need to add the SessionStart hook to your Claude Code configuration.
You can set this up either through the interactive /hooks command or by manually editing your settings file. See Settings File Locations to choose the appropriate location.

  1. Type /hooks in Claude Code
  2. Select SessionStart - When a new session is started
  3. Select + Add new matcher…
  4. Enter matcher: startup|resume|clear
  5. Select + Add new hook…
  6. Enter command: tdd-guard
  7. Choose where to save

Manual Configuration (Alternative)

Add the following to your chosen settings file:

json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume|clear",
        "hooks": [
          {
            "type": "command",
            "command": "tdd-guard"
          }
        ]
      }
    ]
  }
}

Note: Your configuration file may already have other hooks configured.
Simply add the SessionStart section to your existing hooks object.

How It Works

The SessionStart hook triggers when:

  • Claude Code starts up (startup)
  • A session is resumed (resume)
  • The /clear command is used (clear)

When triggered, TDD Guard clears all transient data while preserving the guard state and your custom validation rules.

Tips

  • No manual intervention needed - clearing happens automatically
  • To toggle the guard on/off, use the quick commands
  • For debugging, check .claude/tdd-guard/ to see stored data

Storybook Vitest Addon (`docs/storybook-vitest-addon.md`)

Storybook with Vitest Addon

If you're using Storybook 10+ with a Vite-based framework, you can use @storybook/addon-vitest instead of @storybook/test-runner. This approach uses TDD Guard's existing Vitest reporter to capture Storybook test results.

Why Use the Vitest Addon?

The Vitest addon offers several advantages:

  • Faster execution - Uses Vitest's browser mode instead of Playwright
  • Modern tooling - Built on Vitest, which is faster and more modern than Jest
  • Unified testing - Run Storybook tests alongside your other Vitest tests
  • Better DX - Full Storybook Test experience with interaction, accessibility, and visual tests
  • Simpler setup - Uses your existing Vitest configuration

Requirements

  • Storybook 10+
  • Vite-based Storybook framework (React-Vite, Vue-Vite, Svelte-Vite, etc.)
  • Vitest

Setup

1. Install the Vitest Addon

bash
npm install --save-dev @storybook/addon-vitest

2. Configure Storybook

Add the addon to your .storybook/main.ts:

typescript
import type { StorybookConfig } from '@storybook/react-vite'

const config: StorybookConfig = {
  stories: ['../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-vitest'],
  framework: '@storybook/react-vite',
}

export default config

3. Configure Vitest Reporter

Since the Vitest addon runs your Storybook tests through Vitest, you use the tdd-guard-vitest reporter to capture results:

bash
npm install --save-dev tdd-guard-vitest

Add to your vitest.config.ts:

typescript
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    reporters: [
      'default',
      ['tdd-guard-vitest', { projectRoot: '/Users/username/projects/my-app' }],
    ],
  },
})

4. Run Tests

bash
npm run test

Your Storybook interaction tests will run alongside your regular Vitest tests, and TDD Guard will capture all results.

Comparison with Test Runner

Feature @storybook/test-runner @storybook/addon-vitest
Test framework Jest + Playwright Vitest browser mode
Storybook version 6.4+ 10+
Framework support All frameworks Vite-based only
TDD Guard reporter tdd-guard-storybook tdd-guard-vitest
Speed Slower (full browser) Faster

When to Use Each

Use @storybook/addon-vitest when:

  • You're on Storybook 10+
  • You're using a Vite-based framework
  • You want faster test execution
  • You want to unify your testing setup

Use @storybook/test-runner when:

  • You're on Storybook 6.4-9.x
  • You need Webpack-based framework support
  • You need full Playwright browser testing

Troubleshooting

Tests Not Running

  1. Verify Storybook is configured with the Vitest addon
  2. Check that your stories have play functions (interaction tests)
  3. Ensure tdd-guard-vitest is in your Vitest reporters

Results Not Captured

  1. Verify tdd-guard-vitest is installed and configured
  2. Check the project root path is correct in the reporter config
  3. Look for .claude/tdd-guard/data/test.json after running tests

Further Reading


Validation Model (`docs/validation-model.md`)

Validation Model Configuration

TDD Guard validates changes using AI. Configure both the validation client (SDK or API) and the Claude model version.

Claude Agent SDK (Default)

The recommended approach. Uses the Claude Agent SDK to communicate with Claude directly.

bash
VALIDATION_CLIENT=sdk  # Default, can be omitted

Features:

  • Works automatically with your Claude Code installation
  • Uses your Claude Code subscription (no extra charges)
  • Requires Claude Code to be installed and authenticated

Important: If you have ANTHROPIC_API_KEY set in your environment, Claude Code may use it for billing instead of your subscription. To avoid unexpected charges:

bash
# Check if API key is set
echo $ANTHROPIC_API_KEY

# Unset it if present
unset ANTHROPIC_API_KEY

If you've never created an API key, you can ignore this warning.

Anthropic API

For CI/CD environments or when you need faster validation. Requires separate billing from Claude Code.

bash
VALIDATION_CLIENT=api
TDD_GUARD_ANTHROPIC_API_KEY=your_api_key_here

Get your API key from console.anthropic.com

Notes:

  • Charges separately from your Claude Code subscription (pricing)
  • We use TDD_GUARD_ANTHROPIC_API_KEY (not ANTHROPIC_API_KEY) to prevent accidental charges. If you used the regular ANTHROPIC_API_KEY, Claude Code might use it for all your normal coding tasks, charging your API account instead of using your subscription.

Model Selection

Configure which Claude model to use for validation (default: claude-sonnet-4-6):

bash
# Fastest but unreliable results
TDD_GUARD_MODEL_VERSION=claude-3-5-haiku-20241022

# Best results but slowest
TDD_GUARD_MODEL_VERSION=claude-opus-4-1

See Claude model overview for available models and pricing. Note: pricing only applies to API users - SDK uses your Claude Code subscription by default. Balance model capability with custom instructions to optimize for your needs.

Migration from Legacy Configuration

If you're using the old MODEL_TYPE configuration, see the Configuration Migration Guide for detailed instructions.


Adr 001 Claude Session Subdirectory (`docs/adr/001-claude-session-subdirectory.md`)

ADR-001: Execute Claude CLI from Subdirectory for Session Management

Status

Accepted

Context

The TDD Guard validation system creates a new Claude session for each validation operation when running claude commands. This results in session list clutter and makes it difficult to track the actual development sessions.

We considered two approaches:

  1. Clear the context before each validation using /clear command
  2. Run the Claude CLI from a subdirectory to isolate validation sessions

The first approach has limitations:

  • Cannot clear and ask a question in the same command
  • Would require multiple command executions
  • Still shows all sessions in the same directory listing

Decision

We will execute all Claude validation commands from a .claude subdirectory within the project root.

Implementation details:

  • Create .claude directory if it doesn't exist
  • Set the cwd option in execSync to the subdirectory path
  • The .claude directory itself is not ignored (contains settings.json which should be tracked)
  • User-specific files like .claude/settings.local.json should already be in .gitignore

Consequences

Positive

  • Validation sessions are isolated from development sessions
  • No cluttering of the main project's session list
  • Automatic trust inheritance from parent directory
  • Simple implementation with minimal code changes
  • No impact on validation functionality

Negative

  • Creates an additional directory in the project
  • Slightly increases complexity in the model client
  • Sessions are less visible (though this is mostly a benefit)

Neutral

  • All validation sessions will appear in the .claude subdirectory listing
  • Developers need to know to look in .claude for validation session history

Adr 002 Secure Claude Binary Path (`docs/adr/002-secure-claude-binary-path.md`)

ADR-002: Secure Claude Binary Path Configuration

Status

Accepted

Context

CodeQL security scanning identified a potential command injection vulnerability in ClaudeModelClient where the Claude binary path is taken from an environment variable (CLAUDE_BINARY_PATH) and interpolated into a shell command executed via execSync.

The vulnerability occurs because:

  • Environment variables can be manipulated by attackers
  • Shell metacharacters in the path could be interpreted, allowing arbitrary command execution
  • For example, setting CLAUDE_BINARY_PATH="claude; rm -rf /" would execute both commands

We considered several approaches:

  1. Use execFileSync instead of execSync - Avoids shell interpretation entirely
  2. Validate/sanitize the binary path - Check for allowed characters only
  3. Use shell-quote library - Properly escape shell metacharacters
  4. Boolean flag for predefined paths - Switch between hardcoded safe paths

Decision

We will use a boolean environment variable USE_LOCAL_CLAUDE to switch between two hardcoded, safe paths:

  • When USE_LOCAL_CLAUDE=true: Use $HOME/.claude/local/claude
  • Otherwise: Use system claude command

Additionally, we will:

  • Implement the path logic in ClaudeModelClient rather than Config class
  • Use execFileSync instead of execSync to prevent shell interpretation
  • Keep the Config class focused on just providing the boolean flag

Consequences

Positive

  • Eliminates injection risk - No user-controlled input in command construction
  • Simple and secure - Only two possible paths, both hardcoded
  • Clear intent - Boolean flag clearly indicates local vs system Claude
  • Separation of concerns - Config provides settings, ModelClient handles implementation
  • Future flexibility - ModelClient can handle OS-specific paths internally

Negative

  • Less flexible - Users cannot specify custom installation paths
  • Requires code changes - Adding new paths requires updating the code
  • Platform-specific paths - May need adjustment for different operating systems

Neutral

  • Migration from CLAUDE_BINARY_PATH to USE_LOCAL_CLAUDE for existing users
  • Documentation needs to be updated to reflect the new configuration approach

Adr 003 Remove Configurable Data Directory (`docs/adr/003-remove-configurable-data-directory.md`)

ADR-003: Remove Configurable Data Directory

Status

Accepted

Context

A security review identified a potential path traversal vulnerability in TDD Guard where the data directory path is taken from an environment variable (TDD_DATA_DIR) and used directly for file system operations without validation.

The vulnerability occurs because:

  • Environment variables can be manipulated by attackers
  • Path traversal sequences (../) in the path could escape the intended directory
  • For example, setting TDD_DATA_DIR="../../../../etc" would write files to system directories
  • The application writes files like test.txt, todo.json, and modifications.json to this directory

We considered several approaches:

  1. Validate and sanitize the path - Check for ../ sequences and resolve to absolute paths
  2. Restrict to project subdirectories - Ensure the path stays within the project root
  3. Use a whitelist of allowed paths - Only allow specific predefined directories
  4. Remove the configuration entirely - Hardcode the data directory path

Decision

We will remove the TDD_DATA_DIR environment variable and hardcode the data directory path to .claude/tdd-guard/data in the Config class.

The implementation will:

  • Remove TDD_DATA_DIR from environment variable processing
  • Hardcode dataDir to .claude/tdd-guard/data in the Config constructor
  • Keep the existing Config class interface unchanged for dependent code
  • Remove documentation about TDD_DATA_DIR from .env.example, README, and CLAUDE.md

Consequences

Positive

  • Eliminates path traversal risk - No user-controlled input for file paths
  • Simpler implementation - No validation or sanitization code needed
  • Consistent data location - All TDD Guard data in a predictable location
  • Better security posture - Follows principle of least privilege
  • No breaking changes for code - Config class interface remains the same

Negative

  • Less flexible - Users cannot customize where TDD Guard stores its data
  • Potential disk space issues - Users cannot redirect to different drives/partitions
  • Testing limitations - Integration tests cannot use isolated data directories

Neutral

  • The data stored (test results, todos, modifications) is operational/temporary
  • Most users likely never customized this path anyway
  • Follows the same security-first approach as ADR-002

Adr 004 Monorepo Architecture (`docs/adr/004-monorepo-architecture.md`)

ADR-004: Monorepo Architecture for Multi-Language Support

Status

Accepted

Context

TDD Guard originally published a single package to both npm and PyPI, with all test framework reporters mixed together in the src directory. This created several problems:

  • Language mixing - JavaScript and Python code in the same package
  • Publishing complexity - Single codebase published to multiple package registries
  • Package bloat - Users installed code for all languages even if using only one
  • Contribution barriers - Adding new reporters required navigating the entire codebase

We considered several approaches:

  1. Keep monolithic structure - Continue with mixed languages in one package
  2. Separate repositories - Create individual repos for each reporter
  3. Monorepo with workspaces - Keep one repo but separate packages

Decision

We will restructure TDD Guard as a monorepo using npm workspaces, with each reporter as a separate package.

The new structure:

text
tdd-guard/                  # Main CLI package (npm)
├── src/                    # Core functionality and shared code
└── package.json

reporters/
├── vitest/                 # tdd-guard-vitest package (npm)
│   └── package.json
└── pytest/                 # tdd-guard-pytest package (PyPI)
    └── pyproject.toml

Implementation details:

  • Main package exports shared functionality (Storage, Config, contracts)
  • Each reporter is a standalone package with its own version
  • Vitest reporter imports shared code from 'tdd-guard' package
  • Python reporter is self-contained (no JavaScript dependencies)

Consequences

Positive

  • Clean separation - Each language has its own package and tooling
  • Smaller packages - Users only install what they need
  • Independent releases - Can update reporters without touching others
  • Easier contributions - Clear boundaries for adding new reporters

Negative

  • Multiple packages to maintain - More release overhead
  • Build complexity - Must ensure correct build order during development

Neutral

  • Users now install two packages (CLI + reporter) instead of one
  • Each package has its own documentation and version number

Adr 005 Claude Project Dir Support (`docs/adr/005-claude-project-dir-support.md`)

ADR-005: Support CLAUDE_PROJECT_DIR for Consistent Data Storage

Status

Accepted

Context

TDD Guard stores data in .claude/tdd-guard/data relative to the current working directory. This creates issues when:

  • Users run commands from subdirectories (e.g., cd src && npm test)
  • Claude Code executes commands from different locations within a project
  • Multiple .claude directories are created at different levels

Previously, users had to configure CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR in Claude Code to ensure commands always run from the project root. This required additional configuration and restricted how developers could use Claude Code.

Claude Code provides the CLAUDE_PROJECT_DIR environment variable that always points to the project root, regardless of where commands are executed. This is part of Claude Code's security best practices.

Decision

We will use CLAUDE_PROJECT_DIR when available to determine the base path for TDD Guard's data directory.

The implementation:

  • Check if CLAUDE_PROJECT_DIR is set and valid
  • Use it as the base path for .claude/tdd-guard/data if available
  • Fall back to current working directory if not set
  • Apply security validations to prevent path traversal attacks
  • Reporter-provided projectRoot takes precedence over CLAUDE_PROJECT_DIR

Security validations include:

  • Validate CLAUDE_PROJECT_DIR is an absolute path
  • Prevent path traversal by checking for .. sequences
  • Ensure current working directory is within CLAUDE_PROJECT_DIR

When validation fails, TDD Guard throws a descriptive error and the operation is blocked, preventing any file system access with invalid paths.

Consequences

Positive

  • Consistent data location - Data is always stored at the project root
  • No user configuration needed - Works automatically with Claude Code
  • Better developer experience - Can run commands from any project subdirectory
  • Maintains security - Path validation prevents directory traversal attacks

Negative

  • Additional validation code - Security checks add complexity, but this is centralized in the Config class

Neutral

  • Falls back gracefully when environment variable is not present
  • Replaces the need for CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR configuration

Adr 006 Phpunit Separate Repository (`docs/adr/006-phpunit-separate-repository.md`)

ADR-006: Separate Repository for PHPUnit Reporter

Status

Accepted

Context

We received a contribution for a PHPUnit reporter that allows PHP developers to use TDD Guard with their PHPUnit test suites. However, when attempting to publish this package to Packagist (the PHP package registry), we encountered a fundamental limitation: Packagist requires the composer.json file to be at the root of the repository.

TDD Guard is organized as a monorepo containing:

  • The main CLI tool (TypeScript/npm)
  • Vitest reporter (TypeScript/npm)
  • Pytest reporter (Python/PyPI)
  • PHPUnit reporter (PHP/Packagist)

This structure works well for npm (which supports workspaces) and PyPI (which can build from subdirectories), but Packagist does not support monorepos or packages in subdirectories.

Options Considered

  1. Move composer.json to repository root

    • Would make the entire TDD Guard project appear as a PHP package
    • Users would download all code (TypeScript, Python, etc.) just to get the PHPUnit reporter
    • Conflicts with the project's primary identity as a CLI tool
  2. Private Packagist subscription

    • Supports monorepos but requires paid subscription
    • Adds ongoing cost for an open-source project
    • Creates barrier for community adoption
  3. Manual installation instructions

    • Users would need to configure Composer to use VCS repository
    • Poor developer experience
    • Reduces discoverability and adoption
  4. Separate repository with automated synchronization

    • Maintains single source of truth in main repository
    • Provides standard Packagist installation experience
    • Can be automated with GitHub Actions

Decision

We will create a separate repository (tdd-guard-phpunit) that mirrors the reporters/phpunit directory from the main repository. This mirror will be automatically synchronized using GitHub Actions whenever changes are pushed to the PHPUnit reporter in the main repository.

Implementation Plan

  1. Initial Setup

    • Create tdd-guard-phpunit repository
    • Use git subtree split to extract PHPUnit reporter history
    • Push to new repository maintaining commit history
    • Submit to Packagist for PHP package distribution
  2. Automated Synchronization

    • GitHub Action triggered on pushes to reporters/phpunit/**
    • Uses git subtree to maintain clean history
    • Force pushes to mirror repository to ensure consistency
    • Syncs relevant tags (e.g., phpunit-v*)
  3. Clear Communication

    • Mirror repository README clearly states it's read-only
    • Directs issues and PRs to main repository
    • Explains the monorepo structure and rationale

Consequences

Positive

  • Standard installation: composer require --dev tdd-guard/phpunit
  • Packagist compatibility: Full integration with PHP ecosystem
  • Automated updates: No manual synchronization needed
  • Clean history: Git subtree preserves relevant commit history
  • Single source of truth: All development happens in main repository

Negative

  • Additional complexity: Must maintain synchronization workflow
  • Delayed updates: Packagist updates depend on GitHub Action execution
  • Repository proliferation: Additional repository to manage
  • Potential confusion: Contributors might submit PRs to wrong repository

Security Considerations

  • Uses GitHub Personal Access Token (PAT) with minimal required permissions
  • Token stored as repository secret, only accessible to repository admins
  • Automated workflow reduces human error in synchronization
  • Mirror repository can be made explicitly read-only if needed

Future Considerations

If Packagist adds monorepo support in the future, we could deprecate the mirror repository and publish directly from the main repository. Until then, this approach provides the best balance of maintainability and user experience.

The same pattern could be applied if we need to publish other language-specific packages that don't support monorepos (e.g., RubyGems, CPAN).


Adr 007 Golangci Lint Path Support (`docs/adr/007-golangci-lint-path-support.md`)

ADR-007: Golangci-lint Path and Working Directory Support

Status

Proposed

Context

The golangci-lint integration in TDD Guard faced two specific challenges that required architectural changes to work correctly.

Key problems encountered:

  1. File-based linting limitations - TDD Guard's default linter interface passes absolute file paths to be linted. When golangci-lint receives individual file paths, it cannot resolve other functions and types in the same Go package, leading to false undefined variable/function errors. golangci-lint is designed to work at the package/directory level, not individual files.

  2. Working directory dependency - golangci-lint uses the current working directory to locate the project's go.mod file for module resolution. This TDD Guard repository is primarily TypeScript with Go test artifacts in subdirectories (test/artifacts/go/). When tests run from the project root, golangci-lint fails to find the correct go.mod files in the test artifact directories.

We considered several approaches:

  1. Add module resolution to linter interface - Make the core linter system aware of Go modules and working directories
  2. File-based workarounds - Continue trying to make golangci-lint work with individual files
  3. Directory-based linting with test isolation - Use golangci-lint's natural directory mode and isolate tests that need working directory changes
  4. Separate Go project structure - Move Go artifacts to a different repository

Decision

We will adapt golangci-lint to work within TDD Guard's file-based linter interface using directory-based execution and test isolation, avoiding changes to the core linter architecture.

Implementation details:

Directory-based linting:

  • Extract unique directories from provided file paths using dirname()
  • Pass directories to golangci-lint instead of individual files
  • Use --path-mode=abs flag to support absolute path arguments
  • This allows golangci-lint to resolve Go packages correctly while maintaining the existing linter interface

Test isolation:

  • Configure golangci-lint tests to run in Vitest's fork pool instead of threads pool
  • Use process.chdir() in tests with proper cleanup via beforeEach/afterEach hooks
  • This allows tests to change working directory to Go test artifacts without affecting other tests

Key changes:

typescript
// Directory-based argument building
const directories = [...new Set(filePaths.map((file) => dirname(file)))]
const args = [
  'run',
  '--output.json.path=stdout',
  '--path-mode=abs',
  ...directories,
]

// Simplified issue parsing (no path resolution needed)
const issues = results.flatMap(toIssue)

Rationale: This approach was chosen because the situation (non-Go project testing Go linters) is rare and doesn't warrant adding brittle module resolution logic to the core linter system. The isolated test approach keeps the complexity contained to where it's needed.

Consequences

Positive

  • Go language linter support - TDD Guard now supports the most popular Go linting solution
  • Proper package resolution - Directory-based linting allows golangci-lint to resolve Go types and functions correctly
  • Alignment with tooling - Uses golangci-lint as intended (package-level analysis)
  • Comprehensive validation - Go developers get the same TDD enforcement as other languages
  • Maintainable architecture - Avoided adding Go-specific complexity to the core linter interface

Negative

  • Test complexity - Requires separate Vitest project configuration and fork pool for isolation
  • Different execution model - Golangci-lint behaves differently than file-based linters (ESLint)
  • Working directory sensitivity - Tests must manage working directory changes carefully

Neutral

  • Directory-based results - May report issues in files not explicitly requested (entire package)

Security Considerations

The golangci-lint integration performs path manipulation by extracting directories from file paths using dirname() and passing them as command line arguments.


Adr 008 Storybook Reporter Design (`docs/adr/008-storybook-reporter-design.md`)

ADR-008: Storybook Reporter Design

Status

Accepted

Context

Storybook interaction tests (stories with play functions) provide valuable component test coverage, but TDD Guard didn't recognize these test failures as valid RED-phase evidence. This prevented developers from using TDD workflows with Storybook for component testing.

We needed to design a reporter that:

  • Captures test results from Storybook's @storybook/test-runner
  • Writes results in TDD Guard's standard format
  • Handles the unique characteristics of Storybook's test model
  • Follows the established patterns from other reporters (Vitest, Jest)

We considered several design decisions:

  1. Test granularity - Should we report all stories or only those with tests?
  2. Module hierarchy - How should we group stories into modules?
  3. Reporter lifecycle - Should we write on each story or accumulate results?
  4. Render failures - How should we handle stories that crash during render?
  5. Data extraction - Should we query the page or use provided context?

Decision

We will implement tdd-guard-storybook with the following design:

Test Granularity

Only report stories with play functions as tests.

Stories without play functions are just renders and don't provide meaningful RED/GREEN signals. This aligns with how other reporters work - they don't report non-test files. TDD Guard requires actual test failures as evidence.

Module/Test Hierarchy

One module per story file, multiple stories become tests within that module.

Structure:

json
{
  "testModules": [
    {
      "moduleId": "src/Button.stories.tsx",
      "tests": [
        {
          "name": "Primary",
          "fullName": "Button > Primary",
          "state": "passed"
        }
      ]
    }
  ]
}

This matches how Vitest/Jest group tests by file. One source file = one module is intuitive and simpler to implement.

Reporter Lifecycle

Accumulate results during postVisit, write once on onExit.

API:

typescript
async postVisit(page, context) {
  await reporter.onStoryResult(context)
}
async onExit() {
  await reporter.onComplete()
}

This matches the Vitest/Jest pattern, is more efficient (one write vs N writes), provides cleaner separation of concerns, and better handles interruptions.

Render Failure Handling

Create synthetic failed test for stories that fail to render.

A story with a play function that crashes during render is still a failing test. This matches VitestReporter pattern for module load failures and provides visibility into all failures, not just interaction test failures.

Data Extraction Strategy

Extract minimal data from test-runner context directly, no additional page queries.

Start simple with what test-runner provides out of the box. Less complexity = fewer failure points. Can enhance later if needed.

Interruption Handling

Best-effort save on exit with "interrupted" status.

Approach:

  • Register process exit handler to catch interruptions
  • Write accumulated results when possible
  • Mark overall status as "interrupted"
  • Partial results better than no results

This matches the Vitest/Jest pattern and TDD Guard already handles "interrupted" status.

Consequences

Positive

  • Consistent patterns - Follows established Vitest/Jest reporter patterns
  • Efficient - Single write operation instead of multiple
  • Complete coverage - Captures both interaction test failures and render failures
  • Simple API - Easy to configure with minimal code
  • Robust - Handles interruptions gracefully
  • TDD-aligned - Only reports actual tests, not passive renders

Negative

  • Stories without play not tracked - But these aren't tests, so this is intentional
  • Accumulates in memory - Could be an issue for extremely large test suites, but matches other reporters
  • Storybook-specific - Requires @storybook/test-runner as peer dependency

Neutral

  • Users need to configure .storybook/test-runner.ts with the reporter
  • Follows same security validations as other reporters (absolute paths, project root validation)
  • Results saved to standard location: .claude/tdd-guard/data/test.json

Implementation Details

Integration Test Architecture

The Storybook reporter integration tests follow the same real-execution pattern as Jest, Vitest, PHPUnit, Pytest, Go, and Rust reporters.

Test Flow:

text
Integration Test
    ↓
Factory (storybook.ts)
    ↓
Copy story files → Write configs → Spawn test-runner
    ↓
@storybook/test-runner (Jest + Playwright)
    ↓
Executes stories → Calls StorybookReporter hooks
    ↓
StorybookReporter.onStoryResult() → Save to FileStorage
    ↓
Factory reads results → Returns to test assertions

Test Artifacts Structure

text
reporters/test/storybook/
├── .storybook/
│   └── main.js                        # Storybook configuration
├── Calculator.js                      # Simple component module
├── single-passing.stories.js          # Story with passing assertions
├── single-failing.stories.js          # Story with failing expect()
└── single-import-error.stories.js     # Story importing non-existent module

Stories use JavaScript (.stories.js) to match the repository's test file convention (.test.js) and reduce complexity.

Factory Implementation

The reporters/test/factories/storybook.ts factory:

  1. Copies test artifacts - Uses existing copyTestArtifacts helper
  2. Generates test-runner config - Writes test-runner-jest.config.js with reporter
  3. Starts Storybook dev server - Spawns storybook dev on dynamically allocated port via get-port
  4. Executes test-runner - Spawns test-storybook command via spawnSync
  5. Captures results - Reporter saves to FileStorage, factory reads back

Key Design Decisions from Implementation

Test-runner integration: Uses @storybook/test-runner which integrates with Jest's reporter API through postVisit and onExit hooks. The reporter accumulates results during story execution and writes once on completion.

Async server startup: Factory implements timeout-based Storybook server detection by monitoring stdout for "Local:" or port-specific URLs. 240-second timeout accommodates slower container environments.

Optional reporter execution: Integration tests use Promise.allSettled to make reporters optional, allowing tests to run even when some language-specific reporters aren't installed.

Error handling: The reporter captures both interaction test failures and render failures (import errors, component crashes) as synthetic failed tests, ensuring complete coverage of all failure modes.


Adr 009 Relax Reporter Project Root Validation (`docs/adr/009-relax-reporter-project-root-validation.md`)

ADR-009: Relax Reporter Project Root Path Validation

Status

Accepted

Context

TDD Guard reporters write test results to .claude/tdd-guard/data/test.json under a configured project root. The project root setting exists because test runners are often invoked from a subdirectory — directly by developers, by Claude Code, and especially in monorepo and workspace setups. Without it, results land relative to the runner's cwd and the validation hook does not find them.

Reporters currently require this path to be absolute, and the PHPUnit reporter additionally rejects any path containing ... These checks were added early in the project's life, when the agentic-tool ecosystem was still young and there was little established guidance on where defensive layers belonged. Erring on the side of stricter validation was reasonable in that moment.

The ecosystem has since matured. Claude Code and comparable harnesses now provide first-class permission systems and sandboxing; containerized development environments such as devcontainers are a standard isolation pattern; OS-level isolation tooling is widely documented and available. Users running agents on sensitive work are expected to combine these layers as defense-in-depth. Concerns about a tool process writing outside its project are addressed there — at the harness, the container, and the OS — rather than at the application's input validation. TDD Guard's string-level checks on the project root have become atypical for tools at this layer, and the extra caution they represent is now out of step with where these concerns are expected to live.

That extra caution has a real cost in everyday use:

  • Shared repositories — paths differ between developer machines, so a checked-in absolute path only works for one developer.
  • Docker — host and container paths do not match.

Issue #139 surfaced this concretely for PHPUnit in a shared/Docker context.

This ADR is reporter-side only. The strict validation of CLAUDE_PROJECT_DIR in the main tdd-guard CLI/hook (ADR-005) is a distinct concern from the user-facing reporter configuration this ADR addresses, and remains unchanged.

Decision

Reporters will accept relative paths and paths containing .. for their project root configuration:

  • Drop the absolute-path requirement. Relative paths are resolved against the reporter process's current working directory using the language's standard path resolution.
  • Drop the .. rejection (currently only enforced by PHPUnit).
  • Resolve before validating. Any remaining checks operate on the resolved absolute path, not on the user-provided string.

This ADR does not change the hardcoded data directory subpath (.claude/tdd-guard/data, per ADR-003) or the validation of CLAUDE_PROJECT_DIR in the CLI/hook (per ADR-005).

Out of Scope

The following are deliberately deferred to follow-up work:

  • Environment variable fallback (TDD_GUARD_PROJECT_ROOT) for reporters that do not yet support it.
  • Whether to keep, modify, or remove the cwd-within-root sanity check — left untouched by this ADR and revisited separately.
  • Loud-vs-silent failure behavior — pytest, RSpec, and Minitest currently fall back silently when validation fails; whether to make this loud is a separate question.

Consequences

Positive

  • Unblocks shared repositories and Docker — relative paths checked into the repo work regardless of where it is cloned or mounted.
  • Reduces configuration friction — no per-developer overrides or gitignored entries needed for the project root setting alone.

Negative

  • Loses the absolute-path requirement as a misconfiguration signal. A user who passes an unintended path (e.g. a typo) will see results written there silently rather than getting an immediate format error from the reporter.

Neutral

  • The hardcoded data directory remains. Reporters still write to .claude/tdd-guard/data under the configured project root, with no user-controlled subpath.

Security Considerations

The hardcoded data directory subpath (per ADR-003) keeps the blast radius narrow: reporters write a single fixed file (.claude/tdd-guard/data/test.json), so even if an attacker steers the configured project root, the only thing they can affect is that single file under the chosen directory — they cannot choose a different filename or write to multiple paths. Broader containment of untrusted code execution is handled at the harness, container, and OS layers, where the appropriate primitives now exist.


Adr 010 Standardize Project Root Env Var (`docs/adr/010-standardize-project-root-env-var.md`)

ADR-010: Standardize TDD_GUARD_PROJECT_ROOT Across All Reporters

Status

Accepted

Context

TDD Guard reporters write test results to .claude/tdd-guard/data/test.json under a configured project root. The project root must point to a stable location regardless of where the test runner is invoked — otherwise, results land in the wrong directory and the validation hook does not find them.

ADR-009 relaxed the requirement that the project root be an absolute path. This unblocked shared repositories and Docker setups, but relative paths resolve against the process cwd at runtime. When an agent runs tests from a subdirectory, a relative path resolves to the wrong directory.

Not all configuration formats can compute paths at load time. An environment variable sidesteps this limitation — the value is set once per machine or environment and always produces the correct path. Some reporters already support TDD_GUARD_PROJECT_ROOT, but the coverage is incomplete.

Defaulting to cwd when no project root is configured is problematic — it silently writes results to the wrong location when tests run from a subdirectory, which is the exact scenario the project root setting was designed to prevent.

Decision

All reporters will support TDD_GUARD_PROJECT_ROOT as an environment variable for configuring the project root. The precedence chain is:

  • Explicit option (CLI flag, config file parameter, constructor option) takes highest priority.
  • TDD_GUARD_PROJECT_ROOT environment variable is used when no explicit option is provided.
  • Error when neither is configured. Reporters will not silently default to cwd.

Removing the silent cwd fallback is a breaking change. This is intentional — silent misconfiguration is worse than an upfront error asking the user to set the project root.

Claude Code sets CLAUDE_PROJECT_DIR for hook commands, but this variable is not currently available to Bash subprocesses where reporters run. If it becomes available in the future, reporters can read it as an additional fallback between TDD_GUARD_PROJECT_ROOT and the error.

This ADR does not change the hardcoded data directory subpath (.claude/tdd-guard/data, per ADR-003) or the validation of CLAUDE_PROJECT_DIR in the CLI/hook (per ADR-005).

Out of Scope

  • Whether to keep, modify, or remove the cwd-within-root sanity check — deferred per ADR-009.
  • Integration with CLAUDE_PROJECT_DIR — dependent on Claude Code exposing it to Bash subprocesses.

Consequences

Positive

  • Consistent configuration across all reporters. One env var works for every language and framework, regardless of config format.
  • Per-machine configuration without repo changes. Docker, shared repos, and CI each set the env var for their environment.
  • No silent misconfiguration. Removing the cwd fallback prevents test results from landing in the wrong directory.

Negative

  • Breaking change for zero-config users. Users who relied on the cwd default will need to set TDD_GUARD_PROJECT_ROOT or pass an explicit option.

Neutral

  • Existing env var and option users are unaffected. The explicit option still takes precedence, and reporters that already read TDD_GUARD_PROJECT_ROOT see no change in behavior.

--- METRICS ---