Document spec gaps: auth, phase scaling, observability, testing
Address 21 previously undefined behaviors across specs: - Authentication: Replace email/password with OIDC (Pocket-ID) - Cycle tracking: Add fixed-luteal phase scaling formula with examples - Calendar: Document period logging behavior (preserve predictions) - Garmin: Clarify connection is required (no phase-only mode) - Dashboard: Add UI states, dark mode, onboarding, accessibility - Notifications: Document timezone batching approach - New specs: observability.md (health, metrics, logging) - New specs: testing.md (unit + integration strategy) - Main spec: Add backup/recovery, known limitations, API updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
204
specs/testing.md
Normal file
204
specs/testing.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Testing Specification
|
||||
|
||||
## Job to Be Done
|
||||
|
||||
When I make changes to the codebase, I want automated tests to catch regressions, so that I can deploy with confidence.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
PhaseFlow uses **unit and integration tests** with Vitest. End-to-end tests are not required for MVP (authorized skip).
|
||||
|
||||
### Test Types
|
||||
|
||||
| Type | Scope | Tools | Location |
|
||||
|------|-------|-------|----------|
|
||||
| Unit | Pure functions, utilities | Vitest | Colocated `*.test.ts` |
|
||||
| Integration | API routes, PocketBase interactions | Vitest + supertest | Colocated `*.test.ts` |
|
||||
|
||||
## Framework
|
||||
|
||||
**Vitest** - Fast, Vite-native test runner with TypeScript support.
|
||||
|
||||
**Configuration (`vitest.config.ts`):**
|
||||
```typescript
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
reporter: ['text', 'html'],
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Unit Tests
|
||||
|
||||
Test pure functions in isolation without external dependencies.
|
||||
|
||||
### Priority Targets
|
||||
|
||||
| Module | Functions | Priority |
|
||||
|--------|-----------|----------|
|
||||
| `src/lib/cycle.ts` | `getCycleDay`, `getPhase`, `getPhaseConfig` | High |
|
||||
| `src/lib/decision-engine.ts` | `getTrainingDecision` | High |
|
||||
| `src/lib/nutrition.ts` | `getNutritionGuidance`, `getSeeds`, `getMacros` | Medium |
|
||||
| `src/lib/ics.ts` | `generatePhaseEvents`, `generateCalendarFeed` | Medium |
|
||||
| `src/lib/encryption.ts` | `encrypt`, `decrypt` | Medium |
|
||||
|
||||
### Example Test
|
||||
|
||||
```typescript
|
||||
// src/lib/cycle.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { getCycleDay, getPhase } from './cycle';
|
||||
|
||||
describe('getCycleDay', () => {
|
||||
it('returns 1 on period start date', () => {
|
||||
const lastPeriod = new Date('2024-01-01');
|
||||
const today = new Date('2024-01-01');
|
||||
expect(getCycleDay(lastPeriod, 31, today)).toBe(1);
|
||||
});
|
||||
|
||||
it('handles cycle rollover', () => {
|
||||
const lastPeriod = new Date('2024-01-01');
|
||||
const today = new Date('2024-02-01'); // Day 32
|
||||
expect(getCycleDay(lastPeriod, 31, today)).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPhase', () => {
|
||||
it('returns MENSTRUAL for days 1-3', () => {
|
||||
expect(getPhase(1, 31)).toBe('MENSTRUAL');
|
||||
expect(getPhase(3, 31)).toBe('MENSTRUAL');
|
||||
});
|
||||
|
||||
it('scales correctly for 28-day cycle', () => {
|
||||
// LATE_LUTEAL should be days 22-28 for 28-day cycle
|
||||
expect(getPhase(22, 28)).toBe('LATE_LUTEAL');
|
||||
expect(getPhase(21, 28)).toBe('EARLY_LUTEAL');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Integration Tests
|
||||
|
||||
Test API routes and PocketBase interactions.
|
||||
|
||||
### Setup
|
||||
|
||||
Use a test PocketBase instance or PocketBase's testing utilities.
|
||||
|
||||
```typescript
|
||||
// src/test/setup.ts
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const testPb = new PocketBase(process.env.TEST_POCKETBASE_URL);
|
||||
|
||||
beforeAll(async () => {
|
||||
// Setup test data
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
});
|
||||
```
|
||||
|
||||
### Priority Targets
|
||||
|
||||
| Route | Tests |
|
||||
|-------|-------|
|
||||
| `GET /api/today` | Returns decision with valid auth |
|
||||
| `GET /api/cycle/current` | Returns correct phase info |
|
||||
| `POST /api/cycle/period` | Updates user record |
|
||||
| `GET /api/user` | Returns authenticated user |
|
||||
| `PATCH /api/user` | Updates user fields |
|
||||
| `GET /api/health` | Returns health status |
|
||||
|
||||
### Example Test
|
||||
|
||||
```typescript
|
||||
// src/app/api/today/route.test.ts
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { GET } from './route';
|
||||
|
||||
describe('GET /api/today', () => {
|
||||
it('returns 401 without auth', async () => {
|
||||
const request = new Request('http://localhost/api/today');
|
||||
const response = await GET(request);
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
it('returns decision with valid auth', async () => {
|
||||
// Setup authenticated request
|
||||
const request = new Request('http://localhost/api/today', {
|
||||
headers: { Cookie: 'pb_auth=...' },
|
||||
});
|
||||
const response = await GET(request);
|
||||
expect(response.status).toBe(200);
|
||||
|
||||
const data = await response.json();
|
||||
expect(data).toHaveProperty('decision');
|
||||
expect(data).toHaveProperty('cycleDay');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## File Naming
|
||||
|
||||
Tests colocated with source files:
|
||||
|
||||
```
|
||||
src/
|
||||
lib/
|
||||
cycle.ts
|
||||
cycle.test.ts
|
||||
decision-engine.ts
|
||||
decision-engine.test.ts
|
||||
app/
|
||||
api/
|
||||
today/
|
||||
route.ts
|
||||
route.test.ts
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run with coverage
|
||||
npm run test:coverage
|
||||
|
||||
# Run in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run specific file
|
||||
npm test -- src/lib/cycle.test.ts
|
||||
```
|
||||
|
||||
## Coverage Expectations
|
||||
|
||||
No strict coverage thresholds for MVP, but aim for:
|
||||
- 80%+ coverage on `src/lib/` (core logic)
|
||||
- Key API routes tested for auth and happy path
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. All tests pass in CI before merge
|
||||
2. Core decision engine logic has comprehensive tests
|
||||
3. Phase scaling tested for multiple cycle lengths
|
||||
4. API auth tested for protected routes
|
||||
|
||||
## Acceptance Tests
|
||||
|
||||
- [ ] `npm test` runs without errors
|
||||
- [ ] Unit tests cover decision engine logic
|
||||
- [ ] Unit tests cover cycle phase calculations
|
||||
- [ ] Integration tests verify API authentication
|
||||
- [ ] Tests run in CI pipeline
|
||||
Reference in New Issue
Block a user