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>
4.9 KiB
4.9 KiB
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):
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
// 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.
// 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
// 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
# 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
- All tests pass in CI before merge
- Core decision engine logic has comprehensive tests
- Phase scaling tested for multiple cycle lengths
- API auth tested for protected routes
Acceptance Tests
npm testruns without errors- Unit tests cover decision engine logic
- Unit tests cover cycle phase calculations
- Integration tests verify API authentication
- Tests run in CI pipeline