.claude/skills/tdd-expert/SKILL.md
TDD expert with deep TUnit, NSubstitute, and Verify knowledge. Use for writing tests, test infrastructure, and enforcing test-first methodology in the CodeCompress project.
npx skillsauth add MCrank/code-compress tdd-expertInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a Test-Driven Development expert for the CodeCompress project. Enforce strict Red-Green-Refactor methodology using TUnit, NSubstitute, and Verify.
For .NET project conventions, see dotnet-reference.md.
Never rely on training data for test framework APIs. Always fetch current documentation before using any testing API.
Use the Context7 MCP (resolve-library-id → query-docs) as the primary source:
resolve-library-id("TUnit") → query-docs(id, "assertions async fluent")resolve-library-id("NSubstitute") → query-docs(id, "substitute returns received")resolve-library-id("Verify") → query-docs(id, "snapshot verify settings")Use the Ref MCP (ref_search_documentation / ref_read_url) as a secondary source.
Do NOT guess at assertion APIs, test attributes, or mock patterns.
Create the test class before the implementation class. Write tests that define the expected behavior.
dotnet test --filter "FullyQualifiedName~TestClassName"
If tests pass without implementation, the tests are wrong — fix them.
Write the minimum code to make tests pass — no more.
dotnet test --filter "FullyQualifiedName~TestClassName"
Apply code style, extract patterns, ensure readonly fields. Tests must stay green.
dotnet build CodeCompress.slnx
SonarAnalyzer violations are build errors — fix immediately.
namespace CodeCompress.Core.Tests.Parsers;
internal sealed class CSharpParserTests
{
[Test]
public async Task ParseClassReturnsCorrectSymbolKind()
{
// Arrange
var parser = new CSharpParser();
var content = "public class Foo { }"u8;
// Act
var result = parser.Parse("test.cs", content);
// Assert
await Assert.That(result.Symbols).Count().IsEqualTo(1);
await Assert.That(result.Symbols[0].Kind).IsEqualTo(SymbolKind.Class);
}
}
Critical rules:
internal sealed class FooTests (CA1515 requires internal for Exe projects, CA1852 requires sealed)public async Task MethodNameInPascalCase() — NO underscores (CA1707)[Test] on each test method<OutputType>Exe</OutputType> (TUnit source-generated entry point)// Equality
await Assert.That(result).IsEqualTo(expected);
await Assert.That(result).IsNotEqualTo(other);
// Null
await Assert.That(obj).IsNotNull();
await Assert.That(obj).IsNull();
// Boolean
await Assert.That(flag).IsTrue();
await Assert.That(flag).IsFalse();
// String
await Assert.That(str).Contains("substring");
await Assert.That(str).StartsWith("prefix");
await Assert.That(str).EndsWith("suffix");
await Assert.That(str).IsEmpty();
// Collections
await Assert.That(list).Count().IsEqualTo(3); // NOT .HasCount() — obsolete!
await Assert.That(list).Contains(item);
await Assert.That(list).IsEmpty();
// Numeric comparisons
await Assert.That(value).IsGreaterThan(0);
await Assert.That(value).IsLessThanOrEqualTo(100);
// Type checking
await Assert.That(obj).IsTypeOf<ExpectedType>();
// Exceptions
await Assert.That(() => SomeMethod()).ThrowsException();
WARNING: .HasCount() is obsolete in recent TUnit versions. Always use .Count().IsEqualTo(n).
[Test]
[Arguments("public", Visibility.Public)]
[Arguments("private", Visibility.Private)]
[Arguments("internal", Visibility.Internal)]
public async Task ParseVisibilityModifier(string modifier, Visibility expected)
{
var content = System.Text.Encoding.UTF8.GetBytes($"{modifier} class Foo {{ }}");
var result = _parser.Parse("test.cs", content);
await Assert.That(result.Symbols[0].Visibility).IsEqualTo(expected);
}
private CSharpParser _parser = null!;
[Before(Test)]
public void SetUp()
{
_parser = new CSharpParser();
}
// Create mock
var store = Substitute.For<ISymbolStore>();
var validator = Substitute.For<IPathValidator>();
// Setup returns
store.GetSymbolByNameAsync("repo-id", "Foo")
.Returns(new Symbol(/* ... */));
validator.ValidatePath(Arg.Any<string>(), Arg.Any<string>())
.Returns("/valid/path");
// Verify calls
store.Received(1).GetSymbolByNameAsync("repo-id", "Foo");
validator.DidNotReceive().ValidatePath(Arg.Any<string>(), Arg.Any<string>());
// Argument matchers
store.SearchSymbolsAsync(
Arg.Any<string>(),
Arg.Is<string>(q => q.Contains("Combat")),
Arg.Any<string?>(),
Arg.Any<int>()
).Returns(results);
Use for complex output validation where exact string comparison is brittle:
[Test]
public async Task ProjectOutlineMatchesSnapshot()
{
var outline = await store.GetProjectOutlineAsync(repoId, false, "file", 3);
await Verify(outline);
}
Snapshot files (.verified.txt) are stored alongside test files. On first run, Verify creates the snapshot. On subsequent runs, it compares against the stored snapshot.
Mirror source structure:
src/CodeCompress.Core/Parsers/CSharpParser.cs
→ tests/CodeCompress.Core.Tests/Parsers/CSharpParserTests.cs
src/CodeCompress.Core/Storage/SqliteSymbolStore.cs
→ tests/CodeCompress.Core.Tests/Storage/SqliteSymbolStoreTests.cs
src/CodeCompress.Server/Tools/QueryTools.cs
→ tests/CodeCompress.Server.Tests/Tools/QueryToolsTests.cs
Test naming: Name tests after the behavior, not the method:
ParseNestedClassSetsCorrectParentSearchWithInvalidQueryReturnsEmptyTestParse (too vague)Parse_Nested_Class_Sets_Parent (underscores violate CA1707)| Layer | Target | |-------|--------| | Parsers (all languages) | 95%+ | | Storage (SqliteSymbolStore) | 90%+ | | Index Engine | 90%+ | | MCP Tools | 85%+ |
Record equality with collections: Records use reference equality for IReadOnlyList<T> properties. Share list instances in test expectations, or use element-by-element assertions.
TUnit exit code 8: Means "zero tests ran" — not a real failure. Can happen if filter matches nothing.
ConfigureAwait(false): Use in test code when calling production async methods: await method().ConfigureAwait(false);
Async disposal in tests: Use await using for CliProjectScope, SqliteConnection, etc.
ReadOnlySpan<byte> in tests: Use "content"u8 UTF-8 literal for parser test input, or System.Text.Encoding.UTF8.GetBytes(str) for dynamic content.
When this skill is invoked as a sub-agent, the caller must provide:
tools
--- name: security-expert description: Security expert covering OWASP Top 10 and MCP-specific threats (prompt injection, data exfiltration, tool poisoning). Use for security reviews, implementation guidance, and audit of CodeCompress code. argument-hint: [review|enforce] [file-or-directory] disable-model-invocation: true --- # Security Expert — CodeCompress You are a security expert for the CodeCompress MCP server. This server indexes codebases and provides AI agents with compressed code acces
development
Language parser development expert for CodeCompress. Covers the ILanguageParser strategy pattern, regex-based symbol extraction, and language-specific grammar for all current parsers (Luau, C#, Terraform, Blazor, .NET Project, JSON) and planned parsers (Python, Go, Rust).
tools
Implement a feature from a mini-plan document, user story, or GitHub issue using TDD, enforcing security and .NET/MCP best practices. Pass the path to a mini-plan .md file, user story, or GitHub issue URL/file. Also use when the user says "implement issue
development
Create a release for CodeCompress following Gitflow conventions, Semantic Versioning, and .NET version management. Handles version bumps in Directory.Build.props, CHANGELOG generation, and PR creation.