- Introduced new planning files including PROJECT.md, REQUIREMENTS.md, ROADMAP.md, and STATE.md to outline the vision and requirements for SSH multi-context workspaces. - Added ARCHITECTURE.md and CONCERNS.md to detail the codebase structure and address technical debt, known bugs, and security considerations. - Created CONVENTIONS.md to establish coding standards and practices for the project. - Updated .gitignore to exclude demo files and added configuration for planning tools. This commit lays the groundwork for enhancing SSH functionality and user experience in managing multiple workspaces.
13 KiB
Testing Patterns
Analysis Date: 2026-02-12
Test Framework
Runner:
- Vitest 3.1.4
- Config:
/Users/bskim/claude-devtools/vitest.config.ts
Environment:
happy-dom17.4.6 (DOM simulation)- Globals enabled (
describe,it,expectavailable without imports)
Assertion Library:
- Vitest built-in assertions (Chai-compatible API)
Run Commands:
pnpm test # Run all tests
pnpm test:watch # Watch mode
pnpm test:coverage # Coverage report
pnpm test:coverage:critical # Critical path coverage only
pnpm test:chunks # Chunk building tests
pnpm test:semantic # Semantic step extraction
pnpm test:noise # Noise filtering tests
pnpm test:task-filtering # Task tool filtering
Test File Organization
Location:
Co-located in separate test/ directory, mirroring source structure.
Naming:
*.test.ts - matches source file name exactly.
Structure:
test/
├── main/
│ ├── ipc/ # IPC handler tests
│ │ ├── configValidation.test.ts
│ │ └── guards.test.ts
│ ├── services/ # Service layer tests
│ │ ├── analysis/ # ChunkBuilder, etc.
│ │ ├── discovery/ # ProjectPathResolver, SessionSearcher
│ │ ├── infrastructure/ # FileWatcher
│ │ └── parsing/ # MessageClassifier, SessionParser
│ └── utils/ # Utility function tests
│ ├── jsonl.test.ts
│ ├── pathDecoder.test.ts
│ ├── pathValidation.test.ts
│ ├── regexValidation.test.ts
│ └── tokenizer.test.ts
├── renderer/
│ ├── hooks/ # Hook tests
│ │ ├── navigationUtils.test.ts
│ │ ├── useAutoScrollBottom.test.ts
│ │ ├── useSearchContextNavigation.test.ts
│ │ └── useVisibleAIGroup.test.ts
│ ├── store/ # Zustand store slice tests
│ │ ├── notificationSlice.test.ts
│ │ ├── paneSlice.test.ts
│ │ ├── pathResolution.test.ts
│ │ ├── sessionSlice.test.ts
│ │ ├── tabSlice.test.ts
│ │ └── tabUISlice.test.ts
│ └── utils/ # Renderer utilities
│ ├── claudeMdTracker.test.ts
│ ├── dateGrouping.test.ts
│ ├── formatters.test.ts
│ └── pathUtils.test.ts
├── shared/
│ └── utils/ # Shared utilities
│ ├── markdownSearchRendererAlignment.test.ts
│ ├── markdownTextSearch.test.ts
│ ├── modelParser.test.ts
│ └── tokenFormatting.test.ts
├── mocks/ # Test fixtures and mocks
│ └── electronAPI.ts # Mock window.electronAPI
└── setup.ts # Global test setup
Test Structure
Suite Organization:
import { describe, expect, it } from 'vitest';
import { ChunkBuilder } from '../../../../src/main/services/analysis/ChunkBuilder';
import { isAIChunk, isUserChunk } from '../../../../src/main/types';
describe('ChunkBuilder', () => {
const builder = new ChunkBuilder();
describe('buildChunks', () => {
it('should return empty array for empty input', () => {
const chunks = builder.buildChunks([]);
expect(chunks).toEqual([]);
});
it('should filter out sidechain messages', () => {
const messages = [
createMessage({ type: 'user', isSidechain: false }),
createMessage({ type: 'assistant', isSidechain: true }),
];
const chunks = builder.buildChunks(messages);
expect(chunks).toHaveLength(1);
expect(isUserChunk(chunks[0])).toBe(true);
});
});
describe('UserChunk creation', () => {
// Nested describe for logical grouping
});
});
Patterns:
- Top-level
describeper class/module - Nested
describeper method/function - Descriptive
itstatements ("should do X when Y") - Arrange-Act-Assert pattern
Fixtures and Factories
Test Data: Helper functions to create test objects:
/**
* Creates a minimal ParsedMessage for testing.
*/
function createMessage(overrides: Partial<ParsedMessage>): ParsedMessage {
return {
uuid: `msg-${Math.random().toString(36).slice(2, 11)}`,
parentUuid: null,
type: 'user',
timestamp: new Date(),
content: '',
isSidechain: false,
isMeta: false,
toolCalls: [],
toolResults: [],
...overrides,
};
}
/**
* Creates a minimal Process (subagent) for testing.
*/
function createSubagent(overrides: Partial<Process>): Process {
return {
id: `agent-${Math.random().toString(36).slice(2, 11)}`,
filePath: '/path/to/agent.jsonl',
parentTaskId: 'task-1',
description: 'Test subagent',
startTime: new Date(),
endTime: new Date(),
durationMs: 1000,
isOngoing: false,
messages: [],
metrics: {
inputTokens: 100,
outputTokens: 50,
cacheReadTokens: 0,
cacheCreationTokens: 0,
totalTokens: 150,
messageCount: 2,
durationMs: 1000,
},
...overrides,
};
}
Location: Defined in test files (not centralized) for visibility and simplicity.
Mocking
Framework: Vitest built-in mocking (vi.fn(), vi.mock(), vi.spyOn())
ElectronAPI Mock Pattern:
// test/mocks/electronAPI.ts
export interface MockElectronAPI {
getProjects: ReturnType<typeof vi.fn>;
getSessions: ReturnType<typeof vi.fn>;
getSessionsPaginated: ReturnType<typeof vi.fn>;
// ... all IPC methods
}
export function installMockElectronAPI(): MockElectronAPI {
const mock: MockElectronAPI = {
getProjects: vi.fn(),
getSessions: vi.fn(),
// ...
};
vi.stubGlobal('window', {
electronAPI: mock,
});
return mock;
}
// Usage in tests:
import { installMockElectronAPI, type MockElectronAPI } from '../../mocks/electronAPI';
describe('sessionSlice', () => {
let mockAPI: MockElectronAPI;
beforeEach(() => {
mockAPI = installMockElectronAPI();
});
it('should fetch sessions', async () => {
mockAPI.getSessions.mockResolvedValue([/* data */]);
// Test implementation
});
});
Console Mocking:
Automatic via test/setup.ts - all tests fail if unexpected console.error/warn occurs:
// test/setup.ts
beforeEach(() => {
errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
const unexpectedErrors = errorSpy.mock.calls.map(formatConsoleCall);
const unexpectedWarnings = warnSpy.mock.calls.map(formatConsoleCall);
errorSpy.mockRestore();
warnSpy.mockRestore();
expect(unexpectedErrors, `Unexpected console.error calls:\n${unexpectedErrors.join('\n')}`).toEqual([]);
expect(unexpectedWarnings, `Unexpected console.warn calls:\n${unexpectedWarnings.join('\n')}`).toEqual([]);
});
What to Mock:
window.electronAPI- Always mock in renderer tests- File system operations - Mock when testing logic, not I/O
- External dependencies - Mock when testing integration points
What NOT to Mock:
- Internal utilities (test them directly)
- Type guards (pure functions)
- Formatters and transformers (integration is valuable)
Coverage
Requirements: No enforced minimum (quality over coverage)
Provider: v8 (native V8 coverage)
Reporters:
text- Terminal outputjson- Machine-readablehtml- Interactive browser report
View Coverage:
pnpm test:coverage # Full coverage
pnpm test:coverage:critical # Critical paths only
Includes:
src/**/*.tssrc/**/*.tsx
Excludes:
src/**/*.d.ts(type definitions)src/main/index.ts(entry point)src/preload/index.ts(entry point)
Critical Path Config:
Separate config at /Users/bskim/claude-devtools/vitest.critical.config.ts focuses on:
- Chunk building (
ChunkBuilder.test.ts) - Message classification (
MessageClassifier.test.ts) - Session parsing (
SessionParser.test.ts)
Test Types
Unit Tests: Test individual functions, classes, and utilities in isolation.
Example:
describe('pathDecoder', () => {
describe('encodePath', () => {
it('should encode absolute POSIX paths', () => {
expect(encodePath('/Users/username/project')).toBe('-Users-username-project');
});
it('should encode Windows paths', () => {
expect(encodePath('C:\\Users\\username\\project')).toBe('-C:-Users-username-project');
});
});
});
Integration Tests: Test interactions between modules (e.g., ChunkBuilder + MessageClassifier).
Example:
it('should link subagents to AIChunks', () => {
const messages = [
createMessage({ type: 'assistant', toolCalls: [{ isTask: true, id: 'task-1' }] }),
];
const subagents = [
createSubagent({ parentTaskId: 'task-1' }),
];
const chunks = builder.buildChunks(messages, subagents);
const aiChunk = chunks.find(isAIChunk);
expect(aiChunk?.subagents).toHaveLength(1);
});
Store Tests: Test Zustand slice behavior (state updates, async actions).
Example:
describe('sessionSlice', () => {
it('should update sessions on fetch', async () => {
mockAPI.getSessions.mockResolvedValue([
{ id: 'session-1', createdAt: '2024-01-15T10:00:00Z' },
]);
await store.getState().fetchSessions('project-1');
expect(store.getState().sessions).toHaveLength(1);
expect(store.getState().sessionsLoading).toBe(false);
});
});
E2E Tests: Not currently implemented.
Common Patterns
Async Testing:
it('should handle async operations', async () => {
mockAPI.getSessions.mockResolvedValue([/* data */]);
await store.getState().fetchSessions('project-1');
expect(store.getState().sessions).toHaveLength(1);
});
Error Testing:
it('should handle fetch error', async () => {
mockAPI.getSessions.mockRejectedValue(new Error('Network error'));
await store.getState().fetchSessions('project-1');
expect(store.getState().sessionsError).toBe('Network error');
expect(store.getState().sessionsLoading).toBe(false);
});
Timing/Debounce Testing:
it('should debounce rapid calls', async () => {
vi.useFakeTimers();
const callback = vi.fn();
const debounced = debounce(callback, 100);
debounced();
debounced();
debounced();
vi.advanceTimersByTime(100);
expect(callback).toHaveBeenCalledTimes(1);
vi.useRealTimers();
});
State Transitions:
it('should set loading state during fetch', async () => {
mockAPI.getSessions.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve([]), 100))
);
const fetchPromise = store.getState().fetchSessions('project-1');
expect(store.getState().sessionsLoading).toBe(true);
await fetchPromise;
expect(store.getState().sessionsLoading).toBe(false);
});
Test-Specific ESLint Relaxations
Tests use relaxed TypeScript rules (from eslint.config.js):
// Relaxed for tests
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
'@typescript-eslint/no-unsafe-call': 'off',
'@typescript-eslint/no-unsafe-argument': 'off',
'@typescript-eslint/no-unsafe-return': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-floating-promises': 'off',
Rationale: Test code prioritizes readability and flexibility over type safety.
Setup and Teardown
Global Setup:
/Users/bskim/claude-devtools/test/setup.ts runs before each test file:
- Mocks
process.env.HOME - Installs console spies (fail on unexpected errors/warnings)
Per-Suite Setup:
describe('MyService', () => {
let mockAPI: MockElectronAPI;
let store: TestStore;
beforeEach(() => {
mockAPI = installMockElectronAPI();
store = createTestStore();
});
afterEach(() => {
vi.restoreAllMocks();
});
});
Path Aliases in Tests
Vitest config mirrors TypeScript paths:
resolve: {
alias: {
'@shared': resolve(__dirname, 'src/shared'),
'@main': resolve(__dirname, 'src/main'),
'@renderer': resolve(__dirname, 'src/renderer'),
},
}
Use same aliases as source code:
import { ChunkBuilder } from '@main/services';
import { useStore } from '@renderer/store';
import { formatTokens } from '@shared/utils/tokenFormatting';
Best Practices
Test Independence: Each test should run independently - no shared state between tests.
Descriptive Names:
// Good
it('should return empty array when no messages provided', () => {});
// Bad
it('returns []', () => {});
Single Assertion Focus: Prefer multiple small tests over one large test with many assertions.
Factory Functions: Use factory functions for test data creation - more maintainable than inline objects.
Mock Minimally: Only mock external boundaries (IPC, file system) - test real logic.
Console Discipline: If a test legitimately logs errors, it will fail. Either:
- Fix the code to not log errors
- Mock the specific logger call
- Adjust test expectations
Testing analysis: 2026-02-12