mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
docs: map existing codebase
- STACK.md - Technologies and dependencies - ARCHITECTURE.md - System design and patterns - STRUCTURE.md - Directory layout - CONVENTIONS.md - Code style and patterns - TESTING.md - Test structure - INTEGRATIONS.md - External services - CONCERNS.md - Technical debt and issues
This commit is contained in:
389
.planning/codebase/TESTING.md
Normal file
389
.planning/codebase/TESTING.md
Normal file
@@ -0,0 +1,389 @@
|
||||
# Testing Patterns
|
||||
|
||||
**Analysis Date:** 2026-01-27
|
||||
|
||||
## Test Framework
|
||||
|
||||
**Runner:**
|
||||
|
||||
- Vitest 4.0.16 (for unit and integration tests)
|
||||
- Playwright (for E2E tests)
|
||||
- Config: `apps/server/vitest.config.ts`, `libs/*/vitest.config.ts`, `apps/ui/playwright.config.ts`
|
||||
|
||||
**Assertion Library:**
|
||||
|
||||
- Vitest built-in expect assertions
|
||||
- API: `expect().toBe()`, `expect().toEqual()`, `expect().toHaveLength()`, `expect().toHaveProperty()`
|
||||
|
||||
**Run Commands:**
|
||||
|
||||
```bash
|
||||
npm run test # E2E tests (Playwright, headless)
|
||||
npm run test:headed # E2E tests with browser visible
|
||||
npm run test:packages # All shared package unit tests (vitest)
|
||||
npm run test:server # Server unit tests (vitest run)
|
||||
npm run test:server:coverage # Server tests with coverage report
|
||||
npm run test:all # All tests (packages + server)
|
||||
npm run test:unit # Vitest run (all projects)
|
||||
npm run test:unit:watch # Vitest watch mode
|
||||
```
|
||||
|
||||
## Test File Organization
|
||||
|
||||
**Location:**
|
||||
|
||||
- Co-located with source: `src/module.ts` has `tests/unit/module.test.ts`
|
||||
- Server tests: `apps/server/tests/` (separate directory)
|
||||
- Library tests: `libs/*/tests/` (each package)
|
||||
- E2E tests: `apps/ui/tests/` (Playwright)
|
||||
|
||||
**Naming:**
|
||||
|
||||
- Pattern: `{moduleName}.test.ts` for unit tests
|
||||
- Pattern: `{moduleName}.spec.ts` for specification tests
|
||||
- Glob pattern: `tests/**/*.test.ts`, `tests/**/*.spec.ts`
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
apps/server/
|
||||
├── tests/
|
||||
│ ├── setup.ts # Global test setup
|
||||
│ ├── unit/
|
||||
│ │ ├── providers/ # Provider tests
|
||||
│ │ │ ├── claude-provider.test.ts
|
||||
│ │ │ ├── codex-provider.test.ts
|
||||
│ │ │ └── base-provider.test.ts
|
||||
│ │ └── services/
|
||||
│ └── utils/
|
||||
│ └── helpers.ts # Test utilities
|
||||
└── src/
|
||||
|
||||
libs/platform/
|
||||
├── tests/
|
||||
│ ├── paths.test.ts
|
||||
│ ├── security.test.ts
|
||||
│ ├── subprocess.test.ts
|
||||
│ └── node-finder.test.ts
|
||||
└── src/
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
**Suite Organization:**
|
||||
|
||||
```typescript
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { FeatureLoader } from '@/services/feature-loader.js';
|
||||
|
||||
describe('feature-loader.ts', () => {
|
||||
let featureLoader: FeatureLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
featureLoader = new FeatureLoader();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Cleanup resources
|
||||
});
|
||||
|
||||
describe('methodName', () => {
|
||||
it('should do specific thing', () => {
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Patterns:**
|
||||
|
||||
- Setup pattern: `beforeEach()` initializes test instance, clears mocks
|
||||
- Teardown pattern: `afterEach()` cleans up temp directories, removes created files
|
||||
- Assertion pattern: one logical assertion per test (or multiple closely related)
|
||||
- Test isolation: each test runs with fresh setup
|
||||
|
||||
## Mocking
|
||||
|
||||
**Framework:**
|
||||
|
||||
- Vitest `vi` module: `vi.mock()`, `vi.mocked()`, `vi.clearAllMocks()`
|
||||
- Mock patterns: module mocking, function spying, return value mocking
|
||||
|
||||
**Patterns:**
|
||||
|
||||
Module mocking:
|
||||
|
||||
```typescript
|
||||
vi.mock('@anthropic-ai/claude-agent-sdk');
|
||||
// In test:
|
||||
vi.mocked(sdk.query).mockReturnValue(
|
||||
(async function* () {
|
||||
yield { type: 'text', text: 'Response 1' };
|
||||
})()
|
||||
);
|
||||
```
|
||||
|
||||
Async generator mocking (for streaming APIs):
|
||||
|
||||
```typescript
|
||||
const generator = provider.executeQuery({
|
||||
prompt: 'Hello',
|
||||
model: 'claude-opus-4-5-20251101',
|
||||
cwd: '/test',
|
||||
});
|
||||
const results = await collectAsyncGenerator(generator);
|
||||
```
|
||||
|
||||
Partial mocking with spies:
|
||||
|
||||
```typescript
|
||||
const provider = new TestProvider();
|
||||
const spy = vi.spyOn(provider, 'getName');
|
||||
spy.mockReturnValue('mocked-name');
|
||||
```
|
||||
|
||||
**What to Mock:**
|
||||
|
||||
- External APIs (Claude SDK, GitHub SDK, cloud services)
|
||||
- File system operations (use temp directories instead when possible)
|
||||
- Network calls
|
||||
- Process execution
|
||||
- Time-dependent operations
|
||||
|
||||
**What NOT to Mock:**
|
||||
|
||||
- Core business logic (test the actual implementation)
|
||||
- Type definitions
|
||||
- Internal module dependencies (test integration with real services)
|
||||
- Standard library functions (fs, path, etc. - use fixtures instead)
|
||||
|
||||
## Fixtures and Factories
|
||||
|
||||
**Test Data:**
|
||||
|
||||
```typescript
|
||||
// Test helper for collecting async generator results
|
||||
async function collectAsyncGenerator<T>(generator: AsyncGenerator<T>): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
for await (const item of generator) {
|
||||
results.push(item);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Temporary directory fixture
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'test-'));
|
||||
projectPath = path.join(tempDir, 'test-project');
|
||||
await fs.mkdir(projectPath, { recursive: true });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
try {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Location:**
|
||||
|
||||
- Inline in test files for simple fixtures
|
||||
- `tests/utils/helpers.ts` for shared test utilities
|
||||
- Factory functions for complex test objects: `createTestProvider()`, `createMockFeature()`
|
||||
|
||||
## Coverage
|
||||
|
||||
**Requirements (Server):**
|
||||
|
||||
- Lines: 60%
|
||||
- Functions: 75%
|
||||
- Branches: 55%
|
||||
- Statements: 60%
|
||||
- Config: `apps/server/vitest.config.ts` with thresholds
|
||||
|
||||
**Excluded from Coverage:**
|
||||
|
||||
- Route handlers: tested via integration/E2E tests
|
||||
- Type re-exports
|
||||
- Middleware: tested via integration tests
|
||||
- Prompt templates
|
||||
- MCP integration: awaits MCP SDK integration tests
|
||||
- Provider CLI integrations: awaits integration tests
|
||||
|
||||
**View Coverage:**
|
||||
|
||||
```bash
|
||||
npm run test:server:coverage # Generate coverage report
|
||||
# Opens HTML report in: apps/server/coverage/index.html
|
||||
```
|
||||
|
||||
**Coverage Tools:**
|
||||
|
||||
- Provider: v8
|
||||
- Reporters: text, json, html, lcov
|
||||
- File inclusion: `src/**/*.ts`
|
||||
- File exclusion: `src/**/*.d.ts`, specific service files in thresholds
|
||||
|
||||
## Test Types
|
||||
|
||||
**Unit Tests:**
|
||||
|
||||
- Scope: Individual functions and methods
|
||||
- Approach: Test inputs → outputs with mocked dependencies
|
||||
- Location: `apps/server/tests/unit/`
|
||||
- Examples:
|
||||
- Provider executeQuery() with mocked SDK
|
||||
- Path construction functions with assertions
|
||||
- Error classification with different error types
|
||||
- Config validation with various inputs
|
||||
|
||||
**Integration Tests:**
|
||||
|
||||
- Scope: Multiple modules working together
|
||||
- Approach: Test actual service calls with real file system or temp directories
|
||||
- Pattern: Setup data → call method → verify results
|
||||
- Example: Feature loader reading/writing feature.json files
|
||||
- Example: Auto-mode service coordinating with multiple services
|
||||
|
||||
**E2E Tests:**
|
||||
|
||||
- Framework: Playwright
|
||||
- Scope: Full user workflows from UI
|
||||
- Location: `apps/ui/tests/`
|
||||
- Config: `apps/ui/playwright.config.ts`
|
||||
- Setup:
|
||||
- Backend server with mock agent enabled
|
||||
- Frontend Vite dev server
|
||||
- Sequential execution (workers: 1) to avoid auth conflicts
|
||||
- Screenshots/traces on failure
|
||||
- Auth: Global setup authentication in `tests/global-setup.ts`
|
||||
- Fixtures: `tests/e2e-fixtures/` for test project data
|
||||
|
||||
## Common Patterns
|
||||
|
||||
**Async Testing:**
|
||||
|
||||
```typescript
|
||||
it('should execute async operation', async () => {
|
||||
const result = await featureLoader.loadFeature(projectPath, featureId);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.id).toBe(featureId);
|
||||
});
|
||||
|
||||
// For streams/generators:
|
||||
const generator = provider.executeQuery({ prompt, model, cwd });
|
||||
const results = await collectAsyncGenerator(generator);
|
||||
expect(results).toHaveLength(2);
|
||||
```
|
||||
|
||||
**Error Testing:**
|
||||
|
||||
```typescript
|
||||
it('should throw error when feature not found', async () => {
|
||||
await expect(featureLoader.getFeature(projectPath, 'nonexistent')).rejects.toThrow('not found');
|
||||
});
|
||||
|
||||
// Testing error classification:
|
||||
const errorInfo = classifyError(new Error('ENOENT'));
|
||||
expect(errorInfo.category).toBe('FileSystem');
|
||||
```
|
||||
|
||||
**Fixture Setup:**
|
||||
|
||||
```typescript
|
||||
it('should create feature with images', async () => {
|
||||
// Setup: create temp feature directory
|
||||
const featureDir = path.join(projectPath, '.automaker', 'features', featureId);
|
||||
await fs.mkdir(featureDir, { recursive: true });
|
||||
|
||||
// Act: perform operation
|
||||
const result = await featureLoader.updateFeature(projectPath, {
|
||||
id: featureId,
|
||||
imagePaths: ['/temp/image.png'],
|
||||
});
|
||||
|
||||
// Assert: verify file operations
|
||||
const migratedPath = path.join(featureDir, 'images', 'image.png');
|
||||
expect(fs.existsSync(migratedPath)).toBe(true);
|
||||
});
|
||||
```
|
||||
|
||||
**Mock Reset Pattern:**
|
||||
|
||||
```typescript
|
||||
// In vitest.config.ts:
|
||||
mockReset: true, // Reset all mocks before each test
|
||||
restoreMocks: true, // Restore original implementations
|
||||
clearMocks: true, // Clear mock call history
|
||||
|
||||
// In test:
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
});
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
|
||||
**Vitest Config Patterns:**
|
||||
|
||||
Server config (`apps/server/vitest.config.ts`):
|
||||
|
||||
- Environment: node
|
||||
- Globals: true (describe/it without imports)
|
||||
- Setup files: `./tests/setup.ts`
|
||||
- Alias resolution: resolves `@automaker/*` to source files for mocking
|
||||
|
||||
Library config:
|
||||
|
||||
- Simpler setup: just environment and globals
|
||||
- Coverage with high thresholds (90%+ lines)
|
||||
|
||||
**Global Setup:**
|
||||
|
||||
```typescript
|
||||
// tests/setup.ts
|
||||
import { vi, beforeEach } from 'vitest';
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
process.env.DATA_DIR = '/tmp/test-data';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
```
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
**Isolation:**
|
||||
|
||||
- Each test is independent (no state sharing)
|
||||
- Cleanup temp files in afterEach
|
||||
- Reset mocks and environment variables in beforeEach
|
||||
|
||||
**Clarity:**
|
||||
|
||||
- Descriptive test names: "should do X when Y condition"
|
||||
- One logical assertion per test
|
||||
- Clear arrange-act-assert structure
|
||||
|
||||
**Speed:**
|
||||
|
||||
- Mock external services
|
||||
- Use in-memory temp directories
|
||||
- Avoid real network calls
|
||||
- Sequential E2E tests to prevent conflicts
|
||||
|
||||
**Maintainability:**
|
||||
|
||||
- Use beforeEach/afterEach for common setup
|
||||
- Extract test helpers to `tests/utils/`
|
||||
- Keep test data simple and local
|
||||
- Mock consistently across tests
|
||||
|
||||
---
|
||||
|
||||
_Testing analysis: 2026-01-27_
|
||||
Reference in New Issue
Block a user