Search

PythonReactSEOPDF
GitHub Skill

tzurot-testing

Contains MANDATORY Vitest testing patterns for Tzurot v3. MUST be consulted before creating any `.test.ts` file. Covers mock factories, fake timers, and promise rejection handling.

lbds137☆ 7 Repository stars1 Repository forks

Installation

Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.

View source ↗Guides

Skill instructions

Tzurot v3 Testing Patterns

Use this skill when: Writing tests, debugging test failures, adding mocks, or working with fake timers.

Quick Reference

# Run all tests
pnpm test

# Run specific service
pnpm --filter @tzurot/ai-worker test

# Run specific file
pnpm test -- MyService.test.ts

# Coverage
pnpm test:coverage
// Basic test structure
import { describe, it, expect, vi, beforeEach } from 'vitest';

describe('MyService', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('should do something', () => {
    expect(result).toBe(expected);
  });
});

Core Principles

  1. Test behavior, not implementation
  2. Colocated tests - MyService.test.ts next to MyService.ts
  3. Mock all external dependencies - Discord, Redis, Prisma, AI
  4. Use fake timers - No real delays in tests

Essential Patterns

Fake Timers (ALWAYS Use)
beforeEach(() => {
  vi.useFakeTimers();
});
afterEach(() => {
  vi.restoreAllMocks();
});

it('should retry with delay', async () => {
  const promise = withRetry(fn);
  await vi.runAllTimersAsync();
  await promise;
});
Promise Rejections with Fake Timers (CRITICAL)
// ❌ WRONG - Causes PromiseRejectionHandledWarning
const promise = asyncFunction();
await vi.runAllTimersAsync(); // Rejection happens here!
await expect(promise).rejects.toThrow(); // Too late

// ✅ CORRECT - Attach handler BEFORE advancing timers
const promise = asyncFunction();
const assertion = expect(promise).rejects.toThrow('Error'); // Handler attached
await vi.runAllTimersAsync(); // Now advance
await assertion; // Await result
Mock Factory Pattern
// Use async factory for vi.mock hoisting
vi.mock('./MyService.js', async () => {
  const { mockMyService } = await import('../test/mocks/MyService.mock.js');
  return mockMyService;
});

// Import accessors after vi.mock
import { getMyServiceMock } from '../test/mocks/index.js';

it('should call service', () => {
  expect(getMyServiceMock().someMethod).toHaveBeenCalled();
});
Common Mocks
// Discord message
function createMockMessage(overrides = {}) {
  return {
    id: '123',
    content: 'test',
    author: { id: 'user-123', bot: false },
    channel: { id: 'channel-123', send: vi.fn() },
    reply: vi.fn().mockResolvedValue({}),
    ...overrides,
  } as unknown as Message;
}

// Prisma
function createMockPrisma() {
  return {
    personality: { findUnique: vi.fn(), findMany: vi.fn() },
    $disconnect: vi.fn(),
  } as unknown as PrismaClient;
}

// Redis
function createMockRedis() {
  return {
    get: vi.fn().mockResolvedValue(null),
    set: vi.fn().mockResolvedValue('OK'),
    ping: vi.fn().mockResolvedValue('PONG'),
  } as unknown as Redis;
}

Test File Naming

Type Pattern Location
Unit *.test.ts Next to source
Component *.component.test.ts Next to source
Integration *.test.ts tests/integration/
Contract *.contract.test.ts common-types/types/

Mock Reset Functions

Function What It Does When to Use
vi.clearAllMocks() Clears call history, keeps impl beforeEach()
vi.restoreAllMocks() Restores original (spies only) afterEach()
vi.resetAllMocks() Clears history + resets impl Rarely needed

When to Add Tests

Change Unit Contract Integration
New API endpoint ✅ ✅ Required ✅ If DB/multi-service
New *.service.ts ✅ If shared ✅ For complex DB operations
New utility/helper ✅ No No
Bug fix ✅ If contract If multi-component interaction
New dashboard/modal ✅ If API types No (UI logic, mock sessions)
New tooling ✅ No No
Tooling Package Tests

All tooling code requires unit tests. See tzurot-tooling skill for details. Key points:

  • Implementation modules in packages/tooling/src/*/ need *.test.ts files
  • Command registration files (commands/*.ts) are thin wrappers - no tests needed
  • Mock child_process functions like execSync and spawnSync for shell commands
Integration Test Guidance

When Required:

  • Database operations with complex queries (joins, transactions)
  • Cross-service communication (bot-client → api-gateway → ai-worker)
  • Business logic spanning multiple services

When NOT Needed:

  • Pure utility functions
  • UI/Discord interaction handlers (mock the session/API instead)
  • Simple CRUD operations

Future Enhancement: Service-pairing ratchet where every *.service.ts requires *.integration.test.ts

Contract Tests

Contract tests verify API boundaries between services. Located in common-types/types/.

// *.contract.test.ts - Verify schema compatibility
import { PersonaResponseSchema } from './schemas.js';

describe('PersonaResponse contract', () => {
  it('should parse valid API response', () => {
    const response = { id: 'uuid', name: 'Test', preferredName: null };
    expect(() => PersonaResponseSchema.parse(response)).not.toThrow();
  });

  it('should reject invalid response', () => {
    const response = { id: 123 }; // Wrong type
    expect(() => PersonaResponseSchema.parse(response)).toThrow();
  });
});

When to write: New API endpoints, schema changes, cross-service communication.

Purpose: Catch breaking changes before they hit production. If bot-client expects { name: string } but api-gateway returns { displayName: string }, contract tests fail.

Integration Tests

Integration tests verify multiple components working together. Located in tests/integration/.

// Test actual service interactions (with mocked externals)
describe('AI generation flow', () => {
  it('should process job through full pipeline', async () => {
    // Setup: Create test job data
    const jobData = createTestGenerationJob();

    // Act: Process through actual handlers (mocking only AI/Discord)
    const result = await processGenerationJob(jobData);

    // Assert: Verify end-to-end behavior
    expect(result.response).toBeDefined();
    expect(mockDiscordWebhook).toHaveBeenCalled();
  });
});

When to write: Complex workflows, cross-service operations, database interactions.

Key difference:

  • Unit tests: Mock all dependencies, test one function
  • Integration tests: Use real components (except external APIs like Discord, OpenRouter)
PGLite for Local Integration Tests
# Run integration tests (no DATABASE_URL needed)
pnpm test:integration

# Regenerate schema after Prisma migrations
./scripts/testing/regenerate-pglite-schema.sh

📚 See: docs/reference/testing/PGLITE_SETUP.md for full setup, environment detection, and test patterns.

Definition of Done

Before marking a feature complete:

  • New service files have .component.test.ts
  • New API schemas have .contract.test.ts (if crossing service boundary)
  • Complex DB operations have integration test coverage
  • Coverage doesn't drop (Codecov enforces 80% threshold)
  • Run pnpm ops test:audit to verify no new test gaps

Test Coverage Audits (Ratchet System)

The project uses ratchets to prevent new untested code:

# Run both audits (CI does this automatically)
pnpm ops test:audit

# Contract coverage only
pnpm ops test:audit-contracts

# Service integration coverage only
pnpm ops test:audit-services

# Update baseline (after closing gaps)
pnpm ops test:audit-contracts --update
pnpm ops test:audit-services --update

# Strict mode (fails on ANY gap, not just new ones)
pnpm ops test:audit --strict

📚 See: docs/reference/testing/COVERAGE_AUDIT_SYSTEM.md for detailed audit workflows, chip-away process, and priority order.

Anti-Patterns

// ❌ BAD - Testing private methods
expect(service['privateMethod']()).toBe(value);

// ❌ BAD - Real delays
await new Promise(r => setTimeout(r, 1000));

// ❌ BAD - console.log in tests
console.log('Debug:', value);

// ❌ BAD - Skipping instead of fixing
it.skip('broken test', () => {});

Coverage Requirements

Threshold: 80% project-wide and per-patch. Codecov blocks if coverage drops >2%.

pnpm test:coverage                                  # Check coverage locally
pnpm --filter @tzurot/api-gateway test:coverage     # Specific service

📚 See: docs/reference/testing/COVERAGE_AUDIT_SYSTEM.md for detailed thresholds and reading coverage data.

Related Skills

  • tzurot-code-quality - Lint rules, refactoring patterns
  • tzurot-types - Type-safe test fixtures
  • tzurot-git-workflow - Run tests before pushing
  • tzurot-observability - Mock logger in tests

References

  • Full testing guide: docs/guides/TESTING.md
  • Mock factories: services/*/src/test/mocks/
  • Global philosophy: ~/.claude/CLAUDE.md#universal-testing-philosophy
  • PGLite setup: tests/integration/setup.ts
  • Test audit commands: pnpm ops test:audit-*
  • Schema regeneration: ./scripts/testing/regenerate-pglite-schema.sh
  • Contract baseline: contract-coverage-baseline.json
  • Service baseline: service-integration-baseline.json

Examples

Read the usage examples in the source repository.

View source ↗

Source history

Indexed on
2026-01-20
Index updated
2026-01-26
Branch
main

Index and sync dates describe this directory. View the source repository for code history.

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!