Skip to main content

Overview

T3 Code uses Vitest for all testing across the monorepo. The test suite includes unit tests, integration tests, and browser tests for React components.
NEVER run bun test. Always use bun run test which properly runs Vitest.

Running Tests

All Tests

Run the entire test suite across all packages:
bun run test
This uses Turbo to run tests in parallel across all workspace packages.

Package-Specific Tests

cd apps/server
bun run test

Watch Mode

Run tests in watch mode during development:
# In any package directory
vitest

# Or with filter
vitest --watch --testNamePattern="session"

Browser Tests (Web)

The web app includes browser-based component tests:
cd apps/web

# Install Playwright (first time only)
bun run test:browser:install

# Run browser tests
bun run test:browser

Test Structure

Unit Tests

Most files have corresponding .test.ts files in the same directory:
apps/server/src/
├── main.ts
├── main.test.ts              # Unit tests for main.ts
├── wsServer.ts
├── wsServer.test.ts          # Unit tests for wsServer.ts
└── orchestration/
    ├── decider.ts
    └── decider.test.ts       # Unit tests for decider.ts

Integration Tests

Integration tests live in dedicated directories:
apps/server/
├── src/                      # Source code
└── integration/              # Integration tests
    ├── providerService.integration.test.ts
    └── orchestrationEngine.integration.test.ts

Test File Naming

PatternPurposeExample
*.test.tsUnit testswsServer.test.ts
*.integration.test.tsIntegration testsorchestrationEngine.integration.test.ts
*.logic.test.tsLogic/state testscomposer-logic.test.ts

Writing Tests

Basic Test Structure

T3 Code uses Effect-TS with Vitest, providing enhanced testing capabilities:
example.test.ts
import { assert, it } from '@effect/vitest';
import * as Effect from 'effect/Effect';
import { describe } from 'vitest';

describe('MyFeature', () => {
  it.effect('should do something', () =>
    Effect.gen(function* () {
      const result = yield* myOperation();
      assert.strictEqual(result, 'expected');
    })
  );
});

Testing with Effect Services

Use test layers to provide mock services:
service.test.ts
import { assert, it } from '@effect/vitest';
import * as Effect from 'effect/Effect';
import * as Layer from 'effect/Layer';
import { vi } from 'vitest';

import { MyService } from './MyService';

// Create mock implementation
const mockFn = vi.fn(() => Effect.succeed('mocked'));

const TestMyServiceLive = Layer.succeed(MyService, {
  operation: mockFn,
});

it.effect('should call service', () =>
  Effect.gen(function* () {
    const service = yield* MyService;
    const result = yield* service.operation();
    
    assert.strictEqual(result, 'mocked');
    assert.strictEqual(mockFn).toHaveBeenCalledOnce();
  }).pipe(Effect.provide(TestMyServiceLive))
);

Testing React Components

Web components use standard Vitest + React Testing Library patterns:
Component.test.tsx
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import { MyComponent } from './MyComponent';

describe('MyComponent', () => {
  it('renders correctly', () => {
    render(<MyComponent title="Test" />);
    expect(screen.getByText('Test')).toBeInTheDocument();
  });
});

Testing WebSocket Communication

Use MSW (Mock Service Worker) for WebSocket mocking:
wsTransport.test.ts
import { setupServer } from 'msw/node';
import { ws } from 'msw';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';

import { WsTransport } from './wsTransport';

const chat = ws.link('ws://localhost:3773');
const server = setupServer();

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe('WsTransport', () => {
  it('connects and receives messages', async () => {
    server.use(
      chat.addEventListener('connection', ({ client }) => {
        client.send(JSON.stringify({ type: 'connected' }));
      })
    );
    
    const transport = new WsTransport();
    await transport.connect('ws://localhost:3773');
    
    const message = await transport.waitForMessage();
    expect(message.type).toBe('connected');
  });
});

Test Utilities

Effect Test Utilities

Common patterns for Effect-based tests:
import { assert, it } from '@effect/vitest';
import * as Effect from 'effect/Effect';
import * as Exit from 'effect/Exit';

// Test success case
it.effect('succeeds', () =>
  Effect.gen(function* () {
    const result = yield* operation();
    assert.strictEqual(result, expected);
  })
);

// Test failure case
it.effect('fails with error', () =>
  Effect.gen(function* () {
    const exit = yield* Effect.exit(failingOperation());
    assert(Exit.isFailure(exit));
    assert.strictEqual(exit.cause._tag, 'Fail');
  })
);

// Test with timeout
it.effect('completes within timeout', () =>
  Effect.gen(function* () {
    const result = yield* slowOperation().pipe(
      Effect.timeout('1 second')
    );
    assert.isDefined(result);
  })
);

Mock Factories

Create reusable mock factories for common types:
testUtils.ts
import type { ProviderConfig, SessionState } from '@t3tools/contracts';

export function createMockProviderConfig(
  overrides?: Partial<ProviderConfig>
): ProviderConfig {
  return {
    providerId: 'codex',
    model: 'claude-4.5-sonnet',
    workspaceRoot: '/tmp/test',
    ...overrides,
  };
}

export function createMockSessionState(
  overrides?: Partial<SessionState>
): SessionState {
  return {
    sessionId: 'test-session',
    status: 'idle',
    turns: [],
    ...overrides,
  };
}

Test Fixtures

Store test data in __fixtures__ directories:
packages/contracts/src/
├── provider.ts
├── provider.test.ts
└── __fixtures__/
    ├── valid-provider-config.json
    └── invalid-provider-config.json
provider.test.ts
import { readFileSync } from 'node:fs';
import { describe, expect, it } from 'vitest';

const validConfig = JSON.parse(
  readFileSync('__fixtures__/valid-provider-config.json', 'utf-8')
);

describe('ProviderConfig', () => {
  it('parses valid config', () => {
    const result = ProviderConfig.decode(validConfig);
    expect(Effect.runSync(result)).toBeDefined();
  });
});

Test Configuration

Root vitest.config.ts

vitest.config.ts
import * as path from 'node:path';
import { defineConfig } from 'vitest/config';

export default defineConfig({
  resolve: {
    alias: [
      {
        find: /^@t3tools\/contracts$/,
        replacement: path.resolve(
          import.meta.dirname,
          './packages/contracts/src/index.ts'
        ),
      },
    ],
  },
});

Package-Specific Config

Packages can override the root config:
apps/web/vitest.browser.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    browser: {
      enabled: true,
      provider: 'playwright',
      name: 'chromium',
    },
  },
});

Testing Patterns

Testing Provider Adapters

CodexAdapter.test.ts
import { assert, it } from '@effect/vitest';
import * as Effect from 'effect/Effect';
import * as Layer from 'effect/Layer';
import { describe, vi } from 'vitest';

import { CodexAdapter } from './CodexAdapter';

describe('CodexAdapter', () => {
  const mockProcess = {
    send: vi.fn(),
    on: vi.fn(),
    kill: vi.fn(),
  };
  
  const testLayer = Layer.succeed(CodexAdapter, {
    startSession: (config) =>
      Effect.succeed({ sessionId: 'test', process: mockProcess }),
    sendMessage: (sessionId, message) =>
      Effect.sync(() => mockProcess.send(message)),
  });
  
  it.effect('starts session', () =>
    Effect.gen(function* () {
      const adapter = yield* CodexAdapter;
      const session = yield* adapter.startSession({
        workspaceRoot: '/tmp/test',
      });
      
      assert.strictEqual(session.sessionId, 'test');
    }).pipe(Effect.provide(testLayer))
  );
});

Testing Event Sourcing

OrchestrationEngine.test.ts
import { assert, it } from '@effect/vitest';
import * as Effect from 'effect/Effect';
import { describe } from 'vitest';

import { OrchestrationEngine } from './OrchestrationEngine';
import { OrchestrationEventStore } from './OrchestrationEventStore';

describe('OrchestrationEngine', () => {
  it.effect('processes commands and stores events', () =>
    Effect.gen(function* () {
      const engine = yield* OrchestrationEngine;
      const store = yield* OrchestrationEventStore;
      
      // Execute command
      yield* engine.execute({
        type: 'StartTurn',
        sessionId: 'test',
        message: 'Hello',
      });
      
      // Verify events were stored
      const events = yield* store.getEvents('test');
      assert.isTrue(events.length > 0);
      assert.strictEqual(events[0].type, 'TurnStarted');
    }).pipe(Effect.provide(testLayer))
  );
});

Testing WebSocket Protocol

wsServer.test.ts
import { assert, it } from '@effect/vitest';
import * as Effect from 'effect/Effect';
import { describe } from 'vitest';
import WebSocket from 'ws';

import { Server } from './wsServer';

describe('WsServer', () => {
  it.effect('accepts connections and handles messages', () =>
    Effect.gen(function* () {
      const server = yield* Server;
      yield* server.start;
      
      // Create client
      const client = new WebSocket('ws://localhost:3773');
      yield* Effect.promise(() => new Promise(resolve => 
        client.on('open', resolve)
      ));
      
      // Send message
      client.send(JSON.stringify({
        type: 'SendUserMessage',
        message: 'test',
      }));
      
      // Verify response
      const response = yield* Effect.promise(() => new Promise(resolve =>
        client.on('message', data => resolve(JSON.parse(data.toString())))
      ));
      
      assert.isDefined(response);
      client.close();
    }).pipe(Effect.provide(testLayer))
  );
});

Coverage

Generate coverage reports:
vitest run --coverage
Coverage is configured in each package’s vitest.config.ts:
export default defineConfig({
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
      exclude: [
        'node_modules/',
        'dist/',
        '**/*.test.ts',
        '**/__fixtures__/',
      ],
    },
  },
});

CI Integration

Tests run automatically in CI:
.github/workflows/test.yml
name: Test

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v1
        with:
          bun-version: 1.3.9
      - run: bun install
      - run: bun run test
      - run: bun run typecheck

Best Practices

Test Behavior

Test behavior, not implementation details

Mock Layers

Use Effect layers for clean dependency injection in tests

Descriptive Names

Use clear, descriptive test names that explain intent

Fast Tests

Keep unit tests fast; use integration tests for E2E flows
Both bun lint and bun typecheck must pass before tasks are considered complete.

Debugging Tests

Debug Individual Tests

# Run single test file
vitest run src/main.test.ts

# Run with debugger
node --inspect-brk node_modules/vitest/vitest.mjs run src/main.test.ts

VS Code Debugging

Add to .vscode/launch.json:
{
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Current Test",
      "autoAttachChildProcesses": true,
      "skipFiles": ["<node_internals>/**", "**/node_modules/**"],
      "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
      "args": ["run", "${relativeFile}"],
      "smartStep": true,
      "console": "integratedTerminal"
    }
  ]
}

Next Steps

Building

Learn how to build for production

Architecture

Review the system architecture

Build docs developers (and LLMs) love