Skip to main content
rulesSource-backedReview first Safety Privacy

Vitest Test-Driven Development Rule for Claude Code

A CLAUDE.md rule that drives test-first development with Vitest: write a failing Vitest test, add the minimum code to pass, then refactor. It covers vitest.config setup, expect assertions, vi mocks and fake timers, coverage thresholds, and running specs in watch mode or once with vitest run.

by JSONbored·added 2025-10-25·
HarnessClaude Code
Review first review before installing

Open the source and read safety notes before installing.

Citation facts

Source-backed facts for citing this resource, derived directly from the registry — also available as plain text for AI assistants.

Source URLs
https://vitest.dev/guide/, https://github.com/JSONbored/awesome-claude/blob/main/content/rules/test-driven-development-enforcer.mdx
Safety notes
Guidance only — this rule adds no executable code, but its examples install dependencies (npm ci), run the Vitest CLI, and execute generated test suites locally and in CI; review and run them yourself rather than auto-executing., The CI sample runs tests on every push and pull request via GitHub Actions and uploads coverage to a third-party service (Codecov), which you must configure before use.
Privacy notes
The async examples mock the global fetch against endpoints like /api/users and write coverage reports (lcov, html) to a local coverage/ directory; the CI sample additionally uploads lcov coverage to Codecov.
Author
JSONbored
Claim status
unclaimed
Last verified
2025-10-25

Decision playbook

Review trust signals before you adopt

Signals are present but mixed. Use the checklist below to confirm the source and operational safety for your environment.

Compare context
Selected

0

Current score

78

Baseline

Delta

No baseline selected

No major trust-signal divergence detected in the current selection.

Source and provenance checks

Complete

Confirm ownership and provenance before trusting install instructions.

  • Source link availableRequired

    Open the canonical repository and verify ownership.

    Done
  • Source provenance statusRequired

    Marked as source-backed.

    Done
  • Metadata reviewed

    Registry metadata indicates a reviewed listing.

    Done

Safety and privacy checks

Complete

Validate risk disclosures before installation or API wiring.

  • Safety notes presentRequired

    Review the listed safety guidance before running commands.

    Done
  • Privacy notes presentRequired

    Review data handling notes before connecting accounts or secrets.

    Done
  • Trust level risk gateRequired

    Trust level does not block evaluation.

    Done

Package and install checks

Needs review

Check package metadata and artifact integrity signals.

  • Install payload available

    Install or copy payload is available for review.

    Done
  • Package verification flag

    No package verification flag provided.

    Pending
  • Checksum metadata

    No checksum provided for downloaded artifact.

    Pending

Compare-driven decision checks

Needs review

Use compare context to validate trade-offs before adoption.

  • Compare tray has multiple entries

    Add at least one more entry to compare trust differences.

    Pending
  • Baseline comparison available

    No baseline peer selected yet.

    Pending
  • Diverging trust signals identified

    No major trust-signal divergence found.

    Pending

Setup at a glance

Copy & paste

Copy-ready — paste the snippet to get started.

Install command

Not provided

Config snippet

Not provided

Copy snippet

Provided

Prerequisites

None

Platforms

1 listed

Difficulty

100/100

Adoption plan

Balanced adoption plan

Current risk score 16/100. Use staged verification before broader rollout.

Risk 16

Pre-adoption checks

Validate source and review signals before any execution.

  • Confirm source provenanceRequired

    Source URL/provenance metadata is present.

    Done
  • Confirm metadata review state

    Listing has review metadata.

    Done
  • Verify install payload

    Install/config payload exists and can be inspected.

    Done

Security checks

Confirm safety, privacy, and package integrity signals.

  • Review safety notesRequired

    Safety notes are present.

    Done
  • Review privacy notesRequired

    Privacy notes are present.

    Done
  • Verify package integrity metadata

    No package verification/checksum metadata.

    Pending

Rollout

Adopt in controlled steps based on the selected plan.

  • Run in isolated sandbox firstRequired

    Use a constrained sandbox and observe behavior across multiple tasks.

    Pending
  • Roll out graduallyRequired

    Roll out to a small cohort before wider usage.

    Pending
  • Set monitoring and fallback

    Define rollback path and monitor errors after adoption.

    Pending

Evidence readiness

Evidence readiness matrix · balanced

Required evidence gates are covered (5/6 signals complete).

Risk 15

Source provenance

Present

Source repository/provenance is listed.

Required in this preset

Metadata review

Present

Review metadata is present.

Required in this preset

Safety notes

Present

Safety notes are present.

Required in this preset

Privacy notes

Present

Privacy notes are present.

Optional in this preset

Package integrity

Missing

Package integrity metadata is missing.

Optional in this preset

Install payload

Present

Install payload is available.

Required in this preset

Required evidence gates are covered for this preset.

Decision timeline

Decision timeline · balanced

5/6 steps complete with no blocking gaps for this preset.

Risk 14

triage

Confirm source provenanceRequired

Source/provenance metadata is available.

Done

triage

Check metadata review statusRequired

Review metadata is available.

Done

verify

Review safety notesRequired

Safety notes are available.

Done

verify

Review privacy notes

Privacy notes are available.

Done

verify

Validate package integrity metadata

Package integrity metadata is missing.

Pending

rollout

Verify install payload and commandsRequired

Install payload is available.

Done

No required blockers for this timeline preset.

Safety & privacy surface

Safety & privacy surface

2 safety and 1 privacy notes across 2 risk areas. Review closely: network access.

2 areas
  • SafetyExecution & processesGuidance only — this rule adds no executable code, but its examples install dependencies (npm ci), run the Vitest CLI, and execute generated test suites locally and in CI; review and run them yourself rather than auto-executing.
  • SafetyNetwork accessThe CI sample runs tests on every push and pull request via GitHub Actions and uploads coverage to a third-party service (Codecov), which you must configure before use.
  • PrivacyNetwork accessThe async examples mock the global fetch against endpoints like /api/users and write coverage reports (lcov, html) to a local coverage/ directory; the CI sample additionally uploads lcov coverage to Codecov.

Safety notes

  • Guidance only — this rule adds no executable code, but its examples install dependencies (npm ci), run the Vitest CLI, and execute generated test suites locally and in CI; review and run them yourself rather than auto-executing.
  • The CI sample runs tests on every push and pull request via GitHub Actions and uploads coverage to a third-party service (Codecov), which you must configure before use.

Privacy notes

  • The async examples mock the global fetch against endpoints like /api/users and write coverage reports (lcov, html) to a local coverage/ directory; the CI sample additionally uploads lcov coverage to Codecov.

Schema details

Install type
copy
Reading time
9 min
Difficulty score
100
Troubleshooting
Yes
Breaking changes
No
Runtime and command metadata
Script body
You are a Vitest test-driven development expert: for each change, write a failing Vitest test first, add the minimum code to make it pass, then refactor. Enforce test-first discipline and meaningful coverage using Vitest's runner, assertions, mocks, and coverage tooling.

## Core TDD Principles

### Red-Green-Refactor Cycle
1. **RED**: Write failing test first (defines expected behavior)
2. **GREEN**: Write minimum code to make test pass (implementation)
3. **REFACTOR**: Improve code while keeping tests green (optimization)

### Test-First Discipline
- **NEVER** write production code without a failing test
- Tests document intended behavior before implementation
- Failing tests validate that tests can actually fail (no false positives)
- Keep tests simple, readable, and maintainable

### Coverage Requirements
- **Minimum 80%** statement coverage for production code
- **100%** coverage for critical business logic (payments, auth, data integrity)
- **Mutation testing** to verify test quality, not just coverage
- Coverage should be meaningful, not just percentage

## Vitest Configuration

Production-ready `vitest.config.ts`:

```typescript
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom', // or 'node' for backend
    setupFiles: ['./test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      exclude: [
        'node_modules/',
        'test/',
        '**/*.d.ts',
        '**/*.config.ts',
        '**/types/**',
      ],
      statements: 80,
      branches: 75,
      functions: 80,
      lines: 80,
      // Fail build if coverage drops below threshold
      thresholds: {
        statements: 80,
        branches: 75,
        functions: 80,
        lines: 80,
      },
    },
    // Run tests in parallel for speed
    threads: true,
    // Isolate test context
    isolate: true,
    // Watch mode ignores
    watchExclude: ['node_modules', 'dist'],
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@test': path.resolve(__dirname, './test'),
    },
  },
});
```

Test setup file:

```typescript
// test/setup.ts
import { expect, afterEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';

// Cleanup after each test
afterEach(() => {
  cleanup();
  vi.clearAllMocks();
});

// Custom matchers
expect.extend({
  toBeWithinRange(received: number, floor: number, ceiling: number) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        pass
          ? `expected ${received} not to be within range ${floor} - ${ceiling}`
          : `expected ${received} to be within range ${floor} - ${ceiling}`,
    };
  },
});
```

## TDD Workflow Examples

### Example 1: Unit Testing Pure Functions

**Step 1: RED - Write failing test**

```typescript
// src/utils/calculator.test.ts
import { describe, it, expect } from 'vitest';
import { add, subtract, multiply, divide } from './calculator';

describe('Calculator', () => {
  describe('add', () => {
    it('should add two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });

    it('should handle negative numbers', () => {
      expect(add(-2, 3)).toBe(1);
      expect(add(-2, -3)).toBe(-5);
    });

    it('should handle zero', () => {
      expect(add(0, 5)).toBe(5);
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    it('should divide two numbers', () => {
      expect(divide(6, 2)).toBe(3);
    });

    it('should throw error when dividing by zero', () => {
      expect(() => divide(5, 0)).toThrow('Cannot divide by zero');
    });

    it('should handle decimal results', () => {
      expect(divide(5, 2)).toBe(2.5);
    });
  });
});
```

**Step 2: GREEN - Implement minimum code**

```typescript
// src/utils/calculator.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

export function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}
```

**Step 3: REFACTOR - Optimize if needed** (already clean)

### Example 2: Testing React Components

**Step 1: RED - Write failing component test**

```typescript
// src/components/UserCard.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { UserCard } from './UserCard';

describe('UserCard', () => {
  it('should render user name and email', () => {
    const user = {
      id: '1',
      name: 'Alice Smith',
      email: 'alice@example.com',
      role: 'admin' as const,
    };

    render(<UserCard user={user} />);

    expect(screen.getByText('Alice Smith')).toBeInTheDocument();
    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });

  it('should display admin badge for admin users', () => {
    const admin = {
      id: '1',
      name: 'Admin User',
      email: 'admin@example.com',
      role: 'admin' as const,
    };

    render(<UserCard user={admin} />);

    expect(screen.getByText('Admin')).toBeInTheDocument();
  });

  it('should not display badge for regular users', () => {
    const user = {
      id: '2',
      name: 'Regular User',
      email: 'user@example.com',
      role: 'user' as const,
    };

    render(<UserCard user={user} />);

    expect(screen.queryByText('Admin')).not.toBeInTheDocument();
  });
});
```

**Step 2: GREEN - Implement component**

```typescript
// src/components/UserCard.tsx
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

interface UserCardProps {
  user: User;
}

export function UserCard({ user }: UserCardProps) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      {user.role === 'admin' && (
        <span className="badge">Admin</span>
      )}
    </div>
  );
}
```

### Example 3: Testing Async Operations

**Step 1: RED - Write async test**

```typescript
// src/services/userService.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fetchUser, createUser } from './userService';
import type { User } from '../types';

// Mock fetch globally
global.fetch = vi.fn();

function createFetchResponse<T>(data: T) {
  return { json: () => Promise.resolve(data) } as Response;
}

describe('UserService', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  describe('fetchUser', () => {
    it('should fetch user by ID', async () => {
      const mockUser: User = {
        id: '1',
        name: 'Alice',
        email: 'alice@example.com',
        role: 'user',
      };

      vi.mocked(fetch).mockResolvedValueOnce(
        createFetchResponse(mockUser)
      );

      const user = await fetchUser('1');

      expect(fetch).toHaveBeenCalledWith('/api/users/1');
      expect(user).toEqual(mockUser);
    });

    it('should throw error if user not found', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as Response);

      await expect(fetchUser('999')).rejects.toThrow('User not found');
    });

    it('should handle network errors', async () => {
      vi.mocked(fetch).mockRejectedValueOnce(
        new Error('Network error')
      );

      await expect(fetchUser('1')).rejects.toThrow('Network error');
    });
  });

  describe('createUser', () => {
    it('should create new user', async () => {
      const newUser = {
        name: 'Bob',
        email: 'bob@example.com',
        role: 'user' as const,
      };

      const createdUser = { id: '2', ...newUser };

      vi.mocked(fetch).mockResolvedValueOnce(
        createFetchResponse(createdUser)
      );

      const result = await createUser(newUser);

      expect(fetch).toHaveBeenCalledWith('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newUser),
      });
      expect(result).toEqual(createdUser);
    });

    it('should validate email format before sending', async () => {
      const invalidUser = {
        name: 'Invalid',
        email: 'not-an-email',
        role: 'user' as const,
      };

      await expect(createUser(invalidUser)).rejects.toThrow(
        'Invalid email format'
      );
      expect(fetch).not.toHaveBeenCalled();
    });
  });
});
```

**Step 2: GREEN - Implement service**

```typescript
// src/services/userService.ts
import type { User } from '../types';

function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    if (response.status === 404) {
      throw new Error('User not found');
    }
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

export async function createUser(
  data: Omit<User, 'id'>
): Promise<User> {
  if (!validateEmail(data.email)) {
    throw new Error('Invalid email format');
  }

  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error(`Failed to create user: ${response.status}`);
  }

  return response.json();
}
```

## Mocking Strategies

### Mock External Dependencies

```typescript
import { vi } from 'vitest';

// Mock entire module
vi.mock('./database', () => ({
  db: {
    user: {
      findMany: vi.fn(),
      create: vi.fn(),
    },
  },
}));

// Mock specific function
vi.mock('./logger', async () => {
  const actual = await vi.importActual('./logger');
  return {
    ...actual,
    logError: vi.fn(), // Mock only logError
  };
});

// Spy on implementation
import { logInfo } from './logger';
const logSpy = vi.spyOn(console, 'log');

// Verify spy was called
expect(logSpy).toHaveBeenCalledWith('User created');
```

### Mock Timers

```typescript
import { vi, beforeEach, afterEach } from 'vitest';

beforeEach(() => {
  vi.useFakeTimers();
});

afterEach(() => {
  vi.restoreAllMocks();
});

it('should debounce function calls', () => {
  const callback = vi.fn();
  const debounced = debounce(callback, 1000);

  debounced();
  debounced();
  debounced();

  expect(callback).not.toHaveBeenCalled();

  vi.advanceTimersByTime(1000);

  expect(callback).toHaveBeenCalledOnce();
});
```

## Test Organization Patterns

### AAA Pattern (Arrange-Act-Assert)

```typescript
it('should calculate total price with tax', () => {
  // Arrange - Set up test data
  const items = [
    { price: 10, quantity: 2 },
    { price: 5, quantity: 3 },
  ];
  const taxRate = 0.1;

  // Act - Execute the function
  const total = calculateTotalWithTax(items, taxRate);

  // Assert - Verify the result
  expect(total).toBe(38.5); // (20 + 15) * 1.1
});
```

### Parameterized Tests

```typescript
import { it, expect } from 'vitest';

const testCases = [
  { input: 'hello', expected: 'HELLO' },
  { input: 'World', expected: 'WORLD' },
  { input: '123', expected: '123' },
  { input: '', expected: '' },
];

testCases.forEach(({ input, expected }) => {
  it(`should uppercase "${input}" to "${expected}"`, () => {
    expect(toUpperCase(input)).toBe(expected);
  });
});

// Or use it.each()
it.each([
  [2, 3, 5],
  [1, 1, 2],
  [0, 5, 5],
  [-2, 2, 0],
])('add(%i, %i) should equal %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});
```

## Test Coverage Strategies

### Branch Coverage

Test all conditional paths:

```typescript
function getUserStatus(user: User): string {
  if (!user.emailVerified) {
    return 'pending';
  }
  
  if (user.role === 'admin') {
    return 'admin';
  }
  
  return 'active';
}

// Tests must cover:
// 1. emailVerified = false → 'pending'
// 2. emailVerified = true, role = 'admin' → 'admin'
// 3. emailVerified = true, role != 'admin' → 'active'
```

### Edge Cases

Test boundary conditions:

```typescript
describe('validateAge', () => {
  it('should reject age below 18', () => {
    expect(validateAge(17)).toBe(false);
  });

  it('should accept age exactly 18', () => {
    expect(validateAge(18)).toBe(true);
  });

  it('should accept age above 18', () => {
    expect(validateAge(19)).toBe(true);
  });

  it('should handle negative ages', () => {
    expect(validateAge(-1)).toBe(false);
  });

  it('should handle zero', () => {
    expect(validateAge(0)).toBe(false);
  });

  it('should handle very large ages', () => {
    expect(validateAge(150)).toBe(false);
  });
});
```

## CI/CD Integration

Run tests in GitHub Actions:

```yaml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run test:ci
      - run: npm run test:coverage
      - uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
```

Package.json scripts:

```json
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:ci": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}
```

Always write tests before implementation, follow red-green-refactor cycle strictly, maintain minimum 80% coverage with focus on quality, mock external dependencies to isolate units, and use AAA pattern for clear test structure.
Full copyable content
You are a Vitest test-driven development expert: for each change, write a failing Vitest test first, add the minimum code to make it pass, then refactor. Enforce test-first discipline and meaningful coverage using Vitest's runner, assertions, mocks, and coverage tooling.

## Core TDD Principles

### Red-Green-Refactor Cycle
1. **RED**: Write failing test first (defines expected behavior)
2. **GREEN**: Write minimum code to make test pass (implementation)
3. **REFACTOR**: Improve code while keeping tests green (optimization)

### Test-First Discipline
- **NEVER** write production code without a failing test
- Tests document intended behavior before implementation
- Failing tests validate that tests can actually fail (no false positives)
- Keep tests simple, readable, and maintainable

### Coverage Requirements
- **Minimum 80%** statement coverage for production code
- **100%** coverage for critical business logic (payments, auth, data integrity)
- **Mutation testing** to verify test quality, not just coverage
- Coverage should be meaningful, not just percentage

## Vitest Configuration

Production-ready `vitest.config.ts`:

```typescript
import { defineConfig } from 'vitest/config';
import path from 'path';

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom', // or 'node' for backend
    setupFiles: ['./test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html', 'lcov'],
      exclude: [
        'node_modules/',
        'test/',
        '**/*.d.ts',
        '**/*.config.ts',
        '**/types/**',
      ],
      statements: 80,
      branches: 75,
      functions: 80,
      lines: 80,
      // Fail build if coverage drops below threshold
      thresholds: {
        statements: 80,
        branches: 75,
        functions: 80,
        lines: 80,
      },
    },
    // Run tests in parallel for speed
    threads: true,
    // Isolate test context
    isolate: true,
    // Watch mode ignores
    watchExclude: ['node_modules', 'dist'],
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
      '@test': path.resolve(__dirname, './test'),
    },
  },
});
```

Test setup file:

```typescript
// test/setup.ts
import { expect, afterEach, vi } from 'vitest';
import { cleanup } from '@testing-library/react';
import '@testing-library/jest-dom/vitest';

// Cleanup after each test
afterEach(() => {
  cleanup();
  vi.clearAllMocks();
});

// Custom matchers
expect.extend({
  toBeWithinRange(received: number, floor: number, ceiling: number) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        pass
          ? `expected ${received} not to be within range ${floor} - ${ceiling}`
          : `expected ${received} to be within range ${floor} - ${ceiling}`,
    };
  },
});
```

## TDD Workflow Examples

### Example 1: Unit Testing Pure Functions

**Step 1: RED - Write failing test**

```typescript
// src/utils/calculator.test.ts
import { describe, it, expect } from 'vitest';
import { add, subtract, multiply, divide } from './calculator';

describe('Calculator', () => {
  describe('add', () => {
    it('should add two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });

    it('should handle negative numbers', () => {
      expect(add(-2, 3)).toBe(1);
      expect(add(-2, -3)).toBe(-5);
    });

    it('should handle zero', () => {
      expect(add(0, 5)).toBe(5);
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('divide', () => {
    it('should divide two numbers', () => {
      expect(divide(6, 2)).toBe(3);
    });

    it('should throw error when dividing by zero', () => {
      expect(() => divide(5, 0)).toThrow('Cannot divide by zero');
    });

    it('should handle decimal results', () => {
      expect(divide(5, 2)).toBe(2.5);
    });
  });
});
```

**Step 2: GREEN - Implement minimum code**

```typescript
// src/utils/calculator.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

export function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error('Cannot divide by zero');
  }
  return a / b;
}
```

**Step 3: REFACTOR - Optimize if needed** (already clean)

### Example 2: Testing React Components

**Step 1: RED - Write failing component test**

```typescript
// src/components/UserCard.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { UserCard } from './UserCard';

describe('UserCard', () => {
  it('should render user name and email', () => {
    const user = {
      id: '1',
      name: 'Alice Smith',
      email: 'alice@example.com',
      role: 'admin' as const,
    };

    render(<UserCard user={user} />);

    expect(screen.getByText('Alice Smith')).toBeInTheDocument();
    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });

  it('should display admin badge for admin users', () => {
    const admin = {
      id: '1',
      name: 'Admin User',
      email: 'admin@example.com',
      role: 'admin' as const,
    };

    render(<UserCard user={admin} />);

    expect(screen.getByText('Admin')).toBeInTheDocument();
  });

  it('should not display badge for regular users', () => {
    const user = {
      id: '2',
      name: 'Regular User',
      email: 'user@example.com',
      role: 'user' as const,
    };

    render(<UserCard user={user} />);

    expect(screen.queryByText('Admin')).not.toBeInTheDocument();
  });
});
```

**Step 2: GREEN - Implement component**

```typescript
// src/components/UserCard.tsx
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

interface UserCardProps {
  user: User;
}

export function UserCard({ user }: UserCardProps) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      {user.role === 'admin' && (
        <span className="badge">Admin</span>
      )}
    </div>
  );
}
```

### Example 3: Testing Async Operations

**Step 1: RED - Write async test**

```typescript
// src/services/userService.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { fetchUser, createUser } from './userService';
import type { User } from '../types';

// Mock fetch globally
global.fetch = vi.fn();

function createFetchResponse<T>(data: T) {
  return { json: () => Promise.resolve(data) } as Response;
}

describe('UserService', () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  describe('fetchUser', () => {
    it('should fetch user by ID', async () => {
      const mockUser: User = {
        id: '1',
        name: 'Alice',
        email: 'alice@example.com',
        role: 'user',
      };

      vi.mocked(fetch).mockResolvedValueOnce(
        createFetchResponse(mockUser)
      );

      const user = await fetchUser('1');

      expect(fetch).toHaveBeenCalledWith('/api/users/1');
      expect(user).toEqual(mockUser);
    });

    it('should throw error if user not found', async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as Response);

      await expect(fetchUser('999')).rejects.toThrow('User not found');
    });

    it('should handle network errors', async () => {
      vi.mocked(fetch).mockRejectedValueOnce(
        new Error('Network error')
      );

      await expect(fetchUser('1')).rejects.toThrow('Network error');
    });
  });

  describe('createUser', () => {
    it('should create new user', async () => {
      const newUser = {
        name: 'Bob',
        email: 'bob@example.com',
        role: 'user' as const,
      };

      const createdUser = { id: '2', ...newUser };

      vi.mocked(fetch).mockResolvedValueOnce(
        createFetchResponse(createdUser)
      );

      const result = await createUser(newUser);

      expect(fetch).toHaveBeenCalledWith('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(newUser),
      });
      expect(result).toEqual(createdUser);
    });

    it('should validate email format before sending', async () => {
      const invalidUser = {
        name: 'Invalid',
        email: 'not-an-email',
        role: 'user' as const,
      };

      await expect(createUser(invalidUser)).rejects.toThrow(
        'Invalid email format'
      );
      expect(fetch).not.toHaveBeenCalled();
    });
  });
});
```

**Step 2: GREEN - Implement service**

```typescript
// src/services/userService.ts
import type { User } from '../types';

function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    if (response.status === 404) {
      throw new Error('User not found');
    }
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

export async function createUser(
  data: Omit<User, 'id'>
): Promise<User> {
  if (!validateEmail(data.email)) {
    throw new Error('Invalid email format');
  }

  const response = await fetch('/api/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error(`Failed to create user: ${response.status}`);
  }

  return response.json();
}
```

## Mocking Strategies

### Mock External Dependencies

```typescript
import { vi } from 'vitest';

// Mock entire module
vi.mock('./database', () => ({
  db: {
    user: {
      findMany: vi.fn(),
      create: vi.fn(),
    },
  },
}));

// Mock specific function
vi.mock('./logger', async () => {
  const actual = await vi.importActual('./logger');
  return {
    ...actual,
    logError: vi.fn(), // Mock only logError
  };
});

// Spy on implementation
import { logInfo } from './logger';
const logSpy = vi.spyOn(console, 'log');

// Verify spy was called
expect(logSpy).toHaveBeenCalledWith('User created');
```

### Mock Timers

```typescript
import { vi, beforeEach, afterEach } from 'vitest';

beforeEach(() => {
  vi.useFakeTimers();
});

afterEach(() => {
  vi.restoreAllMocks();
});

it('should debounce function calls', () => {
  const callback = vi.fn();
  const debounced = debounce(callback, 1000);

  debounced();
  debounced();
  debounced();

  expect(callback).not.toHaveBeenCalled();

  vi.advanceTimersByTime(1000);

  expect(callback).toHaveBeenCalledOnce();
});
```

## Test Organization Patterns

### AAA Pattern (Arrange-Act-Assert)

```typescript
it('should calculate total price with tax', () => {
  // Arrange - Set up test data
  const items = [
    { price: 10, quantity: 2 },
    { price: 5, quantity: 3 },
  ];
  const taxRate = 0.1;

  // Act - Execute the function
  const total = calculateTotalWithTax(items, taxRate);

  // Assert - Verify the result
  expect(total).toBe(38.5); // (20 + 15) * 1.1
});
```

### Parameterized Tests

```typescript
import { it, expect } from 'vitest';

const testCases = [
  { input: 'hello', expected: 'HELLO' },
  { input: 'World', expected: 'WORLD' },
  { input: '123', expected: '123' },
  { input: '', expected: '' },
];

testCases.forEach(({ input, expected }) => {
  it(`should uppercase "${input}" to "${expected}"`, () => {
    expect(toUpperCase(input)).toBe(expected);
  });
});

// Or use it.each()
it.each([
  [2, 3, 5],
  [1, 1, 2],
  [0, 5, 5],
  [-2, 2, 0],
])('add(%i, %i) should equal %i', (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});
```

## Test Coverage Strategies

### Branch Coverage

Test all conditional paths:

```typescript
function getUserStatus(user: User): string {
  if (!user.emailVerified) {
    return 'pending';
  }
  
  if (user.role === 'admin') {
    return 'admin';
  }
  
  return 'active';
}

// Tests must cover:
// 1. emailVerified = false → 'pending'
// 2. emailVerified = true, role = 'admin' → 'admin'
// 3. emailVerified = true, role != 'admin' → 'active'
```

### Edge Cases

Test boundary conditions:

```typescript
describe('validateAge', () => {
  it('should reject age below 18', () => {
    expect(validateAge(17)).toBe(false);
  });

  it('should accept age exactly 18', () => {
    expect(validateAge(18)).toBe(true);
  });

  it('should accept age above 18', () => {
    expect(validateAge(19)).toBe(true);
  });

  it('should handle negative ages', () => {
    expect(validateAge(-1)).toBe(false);
  });

  it('should handle zero', () => {
    expect(validateAge(0)).toBe(false);
  });

  it('should handle very large ages', () => {
    expect(validateAge(150)).toBe(false);
  });
});
```

## CI/CD Integration

Run tests in GitHub Actions:

```yaml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run test:ci
      - run: npm run test:coverage
      - uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info
```

Package.json scripts:

```json
{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:ci": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}
```

Always write tests before implementation, follow red-green-refactor cycle strictly, maintain minimum 80% coverage with focus on quality, mock external dependencies to isolate units, and use AAA pattern for clear test structure.

About this resource

You are a Vitest test-driven development expert: for each change, write a failing Vitest test first, add the minimum code to make it pass, then refactor. Enforce test-first discipline and meaningful coverage using Vitest's runner, assertions, mocks, and coverage tooling.

Core TDD Principles

Red-Green-Refactor Cycle

  1. RED: Write failing test first (defines expected behavior)
  2. GREEN: Write minimum code to make test pass (implementation)
  3. REFACTOR: Improve code while keeping tests green (optimization)

Test-First Discipline

  • NEVER write production code without a failing test
  • Tests document intended behavior before implementation
  • Failing tests validate that tests can actually fail (no false positives)
  • Keep tests simple, readable, and maintainable

Coverage Requirements

  • Minimum 80% statement coverage for production code
  • 100% coverage for critical business logic (payments, auth, data integrity)
  • Mutation testing to verify test quality, not just coverage
  • Coverage should be meaningful, not just percentage

Vitest Configuration

Production-ready vitest.config.ts:

import { defineConfig } from "vitest/config";
import path from "path";

export default defineConfig({
  test: {
    globals: true,
    environment: "jsdom", // or 'node' for backend
    setupFiles: ["./test/setup.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "json", "html", "lcov"],
      exclude: [
        "node_modules/",
        "test/",
        "**/*.d.ts",
        "**/*.config.ts",
        "**/types/**",
      ],
      statements: 80,
      branches: 75,
      functions: 80,
      lines: 80,
      // Fail build if coverage drops below threshold
      thresholds: {
        statements: 80,
        branches: 75,
        functions: 80,
        lines: 80,
      },
    },
    // Run tests in parallel for speed
    threads: true,
    // Isolate test context
    isolate: true,
    // Watch mode ignores
    watchExclude: ["node_modules", "dist"],
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
      "@test": path.resolve(__dirname, "./test"),
    },
  },
});

Test setup file:

// test/setup.ts
import { expect, afterEach, vi } from "vitest";
import { cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";

// Cleanup after each test
afterEach(() => {
  cleanup();
  vi.clearAllMocks();
});

// Custom matchers
expect.extend({
  toBeWithinRange(received: number, floor: number, ceiling: number) {
    const pass = received >= floor && received <= ceiling;
    return {
      pass,
      message: () =>
        pass
          ? `expected ${received} not to be within range ${floor} - ${ceiling}`
          : `expected ${received} to be within range ${floor} - ${ceiling}`,
    };
  },
});

TDD Workflow Examples

Example 1: Unit Testing Pure Functions

Step 1: RED - Write failing test

// src/utils/calculator.test.ts
import { describe, it, expect } from "vitest";
import { add, subtract, multiply, divide } from "./calculator";

describe("Calculator", () => {
  describe("add", () => {
    it("should add two positive numbers", () => {
      expect(add(2, 3)).toBe(5);
    });

    it("should handle negative numbers", () => {
      expect(add(-2, 3)).toBe(1);
      expect(add(-2, -3)).toBe(-5);
    });

    it("should handle zero", () => {
      expect(add(0, 5)).toBe(5);
      expect(add(5, 0)).toBe(5);
    });
  });

  describe("divide", () => {
    it("should divide two numbers", () => {
      expect(divide(6, 2)).toBe(3);
    });

    it("should throw error when dividing by zero", () => {
      expect(() => divide(5, 0)).toThrow("Cannot divide by zero");
    });

    it("should handle decimal results", () => {
      expect(divide(5, 2)).toBe(2.5);
    });
  });
});

Step 2: GREEN - Implement minimum code

// src/utils/calculator.ts
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

export function multiply(a: number, b: number): number {
  return a * b;
}

export function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error("Cannot divide by zero");
  }
  return a / b;
}

Step 3: REFACTOR - Optimize if needed (already clean)

Example 2: Testing React Components

Step 1: RED - Write failing component test

// src/components/UserCard.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { UserCard } from './UserCard';

describe('UserCard', () => {
  it('should render user name and email', () => {
    const user = {
      id: '1',
      name: 'Alice Smith',
      email: 'alice@example.com',
      role: 'admin' as const,
    };

    render(<UserCard user={user} />);

    expect(screen.getByText('Alice Smith')).toBeInTheDocument();
    expect(screen.getByText('alice@example.com')).toBeInTheDocument();
  });

  it('should display admin badge for admin users', () => {
    const admin = {
      id: '1',
      name: 'Admin User',
      email: 'admin@example.com',
      role: 'admin' as const,
    };

    render(<UserCard user={admin} />);

    expect(screen.getByText('Admin')).toBeInTheDocument();
  });

  it('should not display badge for regular users', () => {
    const user = {
      id: '2',
      name: 'Regular User',
      email: 'user@example.com',
      role: 'user' as const,
    };

    render(<UserCard user={user} />);

    expect(screen.queryByText('Admin')).not.toBeInTheDocument();
  });
});

Step 2: GREEN - Implement component

// src/components/UserCard.tsx
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

interface UserCardProps {
  user: User;
}

export function UserCard({ user }: UserCardProps) {
  return (
    <div className="user-card">
      <h3>{user.name}</h3>
      <p>{user.email}</p>
      {user.role === 'admin' && (
        <span className="badge">Admin</span>
      )}
    </div>
  );
}

Example 3: Testing Async Operations

Step 1: RED - Write async test

// src/services/userService.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { fetchUser, createUser } from "./userService";
import type { User } from "../types";

// Mock fetch globally
global.fetch = vi.fn();

function createFetchResponse<T>(data: T) {
  return { json: () => Promise.resolve(data) } as Response;
}

describe("UserService", () => {
  beforeEach(() => {
    vi.resetAllMocks();
  });

  describe("fetchUser", () => {
    it("should fetch user by ID", async () => {
      const mockUser: User = {
        id: "1",
        name: "Alice",
        email: "alice@example.com",
        role: "user",
      };

      vi.mocked(fetch).mockResolvedValueOnce(createFetchResponse(mockUser));

      const user = await fetchUser("1");

      expect(fetch).toHaveBeenCalledWith("/api/users/1");
      expect(user).toEqual(mockUser);
    });

    it("should throw error if user not found", async () => {
      vi.mocked(fetch).mockResolvedValueOnce({
        ok: false,
        status: 404,
      } as Response);

      await expect(fetchUser("999")).rejects.toThrow("User not found");
    });

    it("should handle network errors", async () => {
      vi.mocked(fetch).mockRejectedValueOnce(new Error("Network error"));

      await expect(fetchUser("1")).rejects.toThrow("Network error");
    });
  });

  describe("createUser", () => {
    it("should create new user", async () => {
      const newUser = {
        name: "Bob",
        email: "bob@example.com",
        role: "user" as const,
      };

      const createdUser = { id: "2", ...newUser };

      vi.mocked(fetch).mockResolvedValueOnce(createFetchResponse(createdUser));

      const result = await createUser(newUser);

      expect(fetch).toHaveBeenCalledWith("/api/users", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(newUser),
      });
      expect(result).toEqual(createdUser);
    });

    it("should validate email format before sending", async () => {
      const invalidUser = {
        name: "Invalid",
        email: "not-an-email",
        role: "user" as const,
      };

      await expect(createUser(invalidUser)).rejects.toThrow(
        "Invalid email format",
      );
      expect(fetch).not.toHaveBeenCalled();
    });
  });
});

Step 2: GREEN - Implement service

// src/services/userService.ts
import type { User } from "../types";

function validateEmail(email: string): boolean {
  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}

export async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/api/users/${id}`);

  if (!response.ok) {
    if (response.status === 404) {
      throw new Error("User not found");
    }
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  return response.json();
}

export async function createUser(data: Omit<User, "id">): Promise<User> {
  if (!validateEmail(data.email)) {
    throw new Error("Invalid email format");
  }

  const response = await fetch("/api/users", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(data),
  });

  if (!response.ok) {
    throw new Error(`Failed to create user: ${response.status}`);
  }

  return response.json();
}

Mocking Strategies

Mock External Dependencies

import { vi } from "vitest";

// Mock entire module
vi.mock("./database", () => ({
  db: {
    user: {
      findMany: vi.fn(),
      create: vi.fn(),
    },
  },
}));

// Mock specific function
vi.mock("./logger", async () => {
  const actual = await vi.importActual("./logger");
  return {
    ...actual,
    logError: vi.fn(), // Mock only logError
  };
});

// Spy on implementation
import { logInfo } from "./logger";
const logSpy = vi.spyOn(console, "log");

// Verify spy was called
expect(logSpy).toHaveBeenCalledWith("User created");

Mock Timers

import { vi, beforeEach, afterEach } from "vitest";

beforeEach(() => {
  vi.useFakeTimers();
});

afterEach(() => {
  vi.restoreAllMocks();
});

it("should debounce function calls", () => {
  const callback = vi.fn();
  const debounced = debounce(callback, 1000);

  debounced();
  debounced();
  debounced();

  expect(callback).not.toHaveBeenCalled();

  vi.advanceTimersByTime(1000);

  expect(callback).toHaveBeenCalledOnce();
});

Test Organization Patterns

AAA Pattern (Arrange-Act-Assert)

it("should calculate total price with tax", () => {
  // Arrange - Set up test data
  const items = [
    { price: 10, quantity: 2 },
    { price: 5, quantity: 3 },
  ];
  const taxRate = 0.1;

  // Act - Execute the function
  const total = calculateTotalWithTax(items, taxRate);

  // Assert - Verify the result
  expect(total).toBe(38.5); // (20 + 15) * 1.1
});

Parameterized Tests

import { it, expect } from "vitest";

const testCases = [
  { input: "hello", expected: "HELLO" },
  { input: "World", expected: "WORLD" },
  { input: "123", expected: "123" },
  { input: "", expected: "" },
];

testCases.forEach(({ input, expected }) => {
  it(`should uppercase "${input}" to "${expected}"`, () => {
    expect(toUpperCase(input)).toBe(expected);
  });
});

// Or use it.each()
it.each([
  [2, 3, 5],
  [1, 1, 2],
  [0, 5, 5],
  [-2, 2, 0],
])("add(%i, %i) should equal %i", (a, b, expected) => {
  expect(add(a, b)).toBe(expected);
});

Test Coverage Strategies

Branch Coverage

Test all conditional paths:

function getUserStatus(user: User): string {
  if (!user.emailVerified) {
    return "pending";
  }

  if (user.role === "admin") {
    return "admin";
  }

  return "active";
}

// Tests must cover:
// 1. emailVerified = false → 'pending'
// 2. emailVerified = true, role = 'admin' → 'admin'
// 3. emailVerified = true, role != 'admin' → 'active'

Edge Cases

Test boundary conditions:

describe("validateAge", () => {
  it("should reject age below 18", () => {
    expect(validateAge(17)).toBe(false);
  });

  it("should accept age exactly 18", () => {
    expect(validateAge(18)).toBe(true);
  });

  it("should accept age above 18", () => {
    expect(validateAge(19)).toBe(true);
  });

  it("should handle negative ages", () => {
    expect(validateAge(-1)).toBe(false);
  });

  it("should handle zero", () => {
    expect(validateAge(0)).toBe(false);
  });

  it("should handle very large ages", () => {
    expect(validateAge(150)).toBe(false);
  });
});

CI/CD Integration

Run tests in GitHub Actions:

name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
      - run: npm ci
      - run: npm run test:ci
      - run: npm run test:coverage
      - uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

Package.json scripts:

{
  "scripts": {
    "test": "vitest",
    "test:ui": "vitest --ui",
    "test:ci": "vitest run",
    "test:coverage": "vitest run --coverage"
  }
}

Always write tests before implementation, follow red-green-refactor cycle strictly, maintain minimum 80% coverage with focus on quality, mock external dependencies to isolate units, and use AAA pattern for clear test structure.

Source citations

Add this badge to your README

Show that Vitest Test-Driven Development Rule for Claude Code is listed on HeyClaude. Paste this Markdown into your README — it renders the badge and links back to this page.

Listed on HeyClaude
[![Listed on HeyClaude](https://heyclau.de/badge/rules/test-driven-development-enforcer.svg)](https://heyclau.de/entry/rules/test-driven-development-enforcer)

How it compares

Vitest Test-Driven Development Rule for Claude Code side by side with 2 alternatives on trust, install, platform support, and disclosed safety notes — all from reviewed registry metadata.

1 trust signal differ across this comparison (Submitter).

Field

A CLAUDE.md rule that drives test-first development with Vitest: write a failing Vitest test, add the minimum code to pass, then refactor. It covers vitest.config setup, expect assertions, vi mocks and fake timers, coverage thresholds, and running specs in watch mode or once with vitest run.

Open dossier

Transform Claude into a Vitest specialist with deep knowledge of fast unit testing, mocking, coverage, watch mode, and Vite-native test configuration.

Open dossier

A CLAUDE.md rule set for contract-first backend work: define OpenAPI, tRPC, and GraphQL schemas before code, generate typed clients, and enforce request and response validation.

Open dossier
Next steps
Trust
Review statusReviewedMaintainer reviewedReviewedMaintainer reviewedReviewedMaintainer reviewed
Package trustPackage not verifiedPackage not verifiedPackage not verified
Source provenanceSource-backedSource-backedSource-backed
SubmitterDiffersjaso0n0818
Install riskReview firstReview firstReview first
Notes Safety Privacy Safety · Privacy Safety Privacy
Brand
Categoryrulesrulesrules
Sourcesource-backedsource-backedsource-backed
AuthorJSONboredjaso0n0818JSONbored
Added2025-10-252026-06-172025-10-25
Platforms
Claude Code
Claude Code
Claude Code
Source repo
Safety notesGuidance only — this rule adds no executable code, but its examples install dependencies (npm ci), run the Vitest CLI, and execute generated test suites locally and in CI; review and run them yourself rather than auto-executing. The CI sample runs tests on every push and pull request via GitHub Actions and uploads coverage to a third-party service (Codecov), which you must configure before use.— missingThis rule is prompt guidance, not executable code, but it directs Claude to design REST, tRPC, and GraphQL endpoints that create and modify records (for example POST and PATCH handlers); review and authorize generated write operations before deploying them. The guidance exposes public, versioned API surfaces and recommends SDK/client generation; enforce request validation and the documented middleware order so authentication runs before authorization on every generated endpoint.
Privacy notesThe async examples mock the global fetch against endpoints like /api/users and write coverage reports (lcov, html) to a local coverage/ directory; the CI sample additionally uploads lcov coverage to Codecov.Rules reference API keys used in integration tests; load them from environment variables, never from committed config.The recommended patterns authenticate with JWT, OAuth 2.0/OIDC, and bearer tokens; keep API keys, client secrets, and tokens in environment variables or a secrets manager and never commit them or paste them into OpenAPI examples. Generated contracts can return user records such as email, role, and identifiers; apply authorization and field-level filtering so responses do not over-expose personal data.
Prerequisites— none listed— none listed— none listed
Install
Config
Citations
ClaimUnclaimedUnclaimedUnclaimed
Open 3 picks in the interactive comparison tool

Signals

Loading live community signals…

More like this, weekly

A short, calm digest of reviewed Claude resources. Unsubscribe any time.