Testing
Testing patterns and conventions for DorkOS
Testing
DorkOS uses Vitest for all testing, with React Testing Library for component tests.
Running Tests
bash pnpm test Runs all tests in watch mode.bash pnpm test -- --run Runs all tests once. Use this for CI.bash pnpm vitest run apps/server/src/services/__tests__/transcript-reader.test.ts
Test File Structure
Tests live alongside source code in __tests__/ directories:
Component Tests
Component tests require the jsdom environment directive and a mock Transport.
Add the environment directive
Every component test file must start with:
/**
* @vitest-environment jsdom
*/Set up the mock Transport
Use createMockTransport() from @dorkos/test-utils and wrap components in TransportProvider:
import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { TransportProvider } from '@/layers/shared/model';
import { createMockTransport } from '@dorkos/test-utils';
const mockTransport = createMockTransport();
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<TransportProvider transport={mockTransport}>
{children}
</TransportProvider>
);
}Write your tests
describe('MyComponent', () => {
it('renders expected content', () => {
render(<MyComponent />, { wrapper: Wrapper });
expect(screen.getByText('Expected')).toBeInTheDocument();
});
});Service Tests
Server tests mock Node.js modules like fs/promises:
import { describe, it, expect, vi } from 'vitest';
vi.mock('fs/promises');
describe('TranscriptReader', () => {
it('returns session when found', async () => {
vi.mocked(readFile).mockResolvedValue(Buffer.from(mockJsonl));
const result = await transcriptReader.getSession('test-id');
expect(result).toEqual(expect.objectContaining({ id: 'test-id' }));
});
});Hook Tests
import { renderHook, waitFor } from '@testing-library/react';
describe('useCustomHook', () => {
it('returns expected state', async () => {
const { result } = renderHook(() => useCustomHook(), {
wrapper: Wrapper,
});
await waitFor(() => {
expect(result.current.data).toBeDefined();
});
});
});Key Conventions
Prop
Type
Mock Browser APIs
Components that use browser APIs like matchMedia need explicit mocks. Add them in beforeAll
and clean up in afterEach.
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});Test Utilities
The @dorkos/test-utils package provides:
createMockTransport()— fully mocked Transport with all required methodsFakeAgentRuntime— fullAgentRuntimeimplementation withvi.fn()spies and scenario queuecollectSseEvents(app, sessionId, content)— supertest helper for SSE integration testsTestScenario— named scenario keys (SimpleText,ToolCall,TodoWrite,Error)testScenarios— scenario builders that produceStreamEventsequences- Mock factories for sessions, messages, and other domain objects
FakeAgentRuntime (Server Route Tests)
Server tests that need an AgentRuntime should use FakeAgentRuntime instead of hand-rolling mock objects:
import { FakeAgentRuntime, TestScenario, testScenarios } from '@dorkos/test-utils';
let fakeRuntime: FakeAgentRuntime;
beforeEach(() => {
fakeRuntime = new FakeAgentRuntime();
fakeRuntime.withScenarios([testScenarios[TestScenario.SimpleText]('Hello')]);
});FakeAgentRuntime implements every method on the AgentRuntime interface. If the interface changes, tests will fail to compile.
Anti-Patterns
// NEVER test implementation details
expect(component.state.isOpen).toBe(true); // test behavior instead
// NEVER use waitFor without an assertion
await waitFor(() => {}); // always include an expect()
// NEVER leave console mocks without cleanup
vi.spyOn(console, 'error'); // add mockRestore in afterEach
// NEVER use arbitrary timeouts
await new Promise((r) => setTimeout(r, 1000)); // use waitFor