agent-ecosystem/.planning/codebase/TESTING.md
matt ae4833e310 feat(planning): add project planning documents and configuration for SSH multi-context workspaces
- 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.
2026-02-12 09:37:58 +09:00

506 lines
13 KiB
Markdown

# Testing Patterns
**Analysis Date:** 2026-02-12
## Test Framework
**Runner:**
- Vitest 3.1.4
- Config: `/Users/bskim/claude-devtools/vitest.config.ts`
**Environment:**
- `happy-dom` 17.4.6 (DOM simulation)
- Globals enabled (`describe`, `it`, `expect` available without imports)
**Assertion Library:**
- Vitest built-in assertions (Chai-compatible API)
**Run Commands:**
```bash
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:**
```typescript
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 `describe` per class/module
- Nested `describe` per method/function
- Descriptive `it` statements ("should do X when Y")
- Arrange-Act-Assert pattern
## Fixtures and Factories
**Test Data:**
Helper functions to create test objects:
```typescript
/**
* 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:**
```typescript
// 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:
```typescript
// 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 output
- `json` - Machine-readable
- `html` - Interactive browser report
**View Coverage:**
```bash
pnpm test:coverage # Full coverage
pnpm test:coverage:critical # Critical paths only
```
**Includes:**
- `src/**/*.ts`
- `src/**/*.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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
it('should handle async operations', async () => {
mockAPI.getSessions.mockResolvedValue([/* data */]);
await store.getState().fetchSessions('project-1');
expect(store.getState().sessions).toHaveLength(1);
});
```
**Error Testing:**
```typescript
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:**
```typescript
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:**
```typescript
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`):
```typescript
// 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:**
```typescript
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:
```typescript
resolve: {
alias: {
'@shared': resolve(__dirname, 'src/shared'),
'@main': resolve(__dirname, 'src/main'),
'@renderer': resolve(__dirname, 'src/renderer'),
},
}
```
Use same aliases as source code:
```typescript
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:**
```typescript
// 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:
1. Fix the code to not log errors
2. Mock the specific logger call
3. Adjust test expectations
---
*Testing analysis: 2026-02-12*