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:
This uses Turbo to run tests in parallel across all workspace packages.
Package-Specific Tests
Server Tests
Web Tests
Contracts 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
Pattern Purpose Example *.test.tsUnit tests wsServer.test.ts*.integration.test.tsIntegration tests orchestrationEngine.integration.test.ts*.logic.test.tsLogic/state tests composer-logic.test.ts
Writing Tests
Basic Test Structure
T3 Code uses Effect-TS with Vitest, providing enhanced testing capabilities:
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:
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:
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:
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:
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
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
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
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
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:
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