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:
@@ -2,11 +2,11 @@
|
||||
|
||||
## Job to Be Done
|
||||
|
||||
When I access PhaseFlow, I want to securely log in with my email, so that my personal health data remains private.
|
||||
When I access PhaseFlow, I want to securely log in with my identity provider, so that my personal health data remains private.
|
||||
|
||||
## Auth Provider
|
||||
|
||||
Using PocketBase for authentication and data storage.
|
||||
Using PocketBase for authentication and data storage, with OIDC (Pocket-ID) as the primary identity provider.
|
||||
|
||||
**Connection:**
|
||||
- `POCKETBASE_URL` environment variable
|
||||
@@ -14,12 +14,30 @@ Using PocketBase for authentication and data storage.
|
||||
|
||||
## Login Flow
|
||||
|
||||
### Email/Password Authentication
|
||||
### OIDC Authentication (Pocket-ID)
|
||||
|
||||
1. User enters email and password on `/login`
|
||||
2. App calls PocketBase `authWithPassword`
|
||||
3. On success, PocketBase sets auth cookie
|
||||
4. User redirected to dashboard
|
||||
1. User clicks "Sign In" on `/login`
|
||||
2. App redirects to Pocket-ID authorization endpoint
|
||||
3. User authenticates with Pocket-ID (handles MFA if configured)
|
||||
4. Pocket-ID redirects back with authorization code
|
||||
5. PocketBase exchanges code for tokens
|
||||
6. User redirected to dashboard
|
||||
|
||||
### OIDC Configuration
|
||||
|
||||
Configure in PocketBase Admin UI: **Settings → Auth providers → OpenID Connect**
|
||||
|
||||
**Required Settings:**
|
||||
- `Client ID` - From Pocket-ID application
|
||||
- `Client Secret` - From Pocket-ID application
|
||||
- `Issuer URL` - Your Pocket-ID instance URL (e.g., `https://id.yourdomain.com`)
|
||||
|
||||
**Environment Variables:**
|
||||
```env
|
||||
POCKETBASE_OIDC_CLIENT_ID=phaseflow
|
||||
POCKETBASE_OIDC_CLIENT_SECRET=xxx
|
||||
POCKETBASE_OIDC_ISSUER_URL=https://id.yourdomain.com
|
||||
```
|
||||
|
||||
### Session Management
|
||||
|
||||
@@ -32,11 +50,8 @@ Using PocketBase for authentication and data storage.
|
||||
### `/login`
|
||||
|
||||
**Elements:**
|
||||
- Email input
|
||||
- Password input
|
||||
- "Sign In" button
|
||||
- "Sign In with Pocket-ID" button
|
||||
- Error message display
|
||||
- Link to password reset (future)
|
||||
|
||||
**Behavior:**
|
||||
- Redirect to `/` on successful login
|
||||
@@ -130,18 +145,37 @@ User profile management:
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Login completes in under 2 seconds
|
||||
1. OIDC login flow completes in under 5 seconds (including redirect)
|
||||
2. Session persists across browser refreshes
|
||||
3. Unauthorized access redirects to login
|
||||
4. User data isolated by authentication
|
||||
|
||||
## Acceptance Tests
|
||||
|
||||
- [ ] Valid credentials authenticate successfully
|
||||
- [ ] Invalid credentials show error message
|
||||
- [ ] OIDC redirect initiates correctly
|
||||
- [ ] Successful OIDC callback creates/updates user
|
||||
- [ ] Session persists after page refresh
|
||||
- [ ] Protected routes redirect when not authenticated
|
||||
- [ ] GET `/api/user` returns current user data
|
||||
- [ ] PATCH `/api/user` updates user record
|
||||
- [ ] Logout clears session completely
|
||||
- [ ] Auth cookie is HttpOnly and Secure
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Open Registration
|
||||
|
||||
When open registration is enabled:
|
||||
- Add `/signup` page with OIDC provider selection
|
||||
- New users created automatically on first OIDC login
|
||||
- Admin approval workflow (optional)
|
||||
|
||||
### Additional OAuth2 Providers
|
||||
|
||||
PocketBase supports multiple OAuth2 providers. Future options:
|
||||
- Google
|
||||
- GitHub
|
||||
- Apple
|
||||
- Other OIDC-compliant providers
|
||||
|
||||
Each provider configured separately in PocketBase Admin UI.
|
||||
|
||||
@@ -55,6 +55,31 @@ Generates new `calendarToken`, invalidating previous subscriptions.
|
||||
}
|
||||
```
|
||||
|
||||
### Period Logging Behavior
|
||||
|
||||
When a user logs a new period start date, the ICS feed updates as follows:
|
||||
|
||||
1. **Current cycle**: Phase events are corrected retroactively to reflect actual dates
|
||||
2. **Original predictions preserved**: Previous predictions for the current cycle are kept as separate events with "(Predicted)" suffix, allowing users to compare predicted vs. actual
|
||||
3. **Older cycles**: Events from previous cycles remain unchanged
|
||||
4. **Future cycles**: Regenerated based on new period date and cycle length
|
||||
|
||||
**Example after logging period on Day 28 of a predicted 31-day cycle:**
|
||||
```ics
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:🩸 Menstrual (Days 1-3)
|
||||
DTSTART;VALUE=DATE:20240128
|
||||
DESCRIPTION:Actual period start
|
||||
END:VEVENT
|
||||
BEGIN:VEVENT
|
||||
SUMMARY:🩸 Menstrual (Predicted)
|
||||
DTSTART;VALUE=DATE:20240131
|
||||
DESCRIPTION:Original prediction - period arrived 3 days early
|
||||
END:VEVENT
|
||||
```
|
||||
|
||||
This creates a built-in accuracy feedback loop for understanding cycle patterns.
|
||||
|
||||
## In-App Calendar
|
||||
|
||||
### Month View (`/calendar`)
|
||||
@@ -107,6 +132,8 @@ Below calendar:
|
||||
- [ ] ICS contains events for next 90 days
|
||||
- [ ] Invalid token returns 401
|
||||
- [ ] Regenerate token creates new unique token
|
||||
- [ ] Period log updates current cycle events retroactively
|
||||
- [ ] Period log preserves original predictions as "(Predicted)" events
|
||||
- [ ] Month view renders all days in month
|
||||
- [ ] Day cells show correct phase colors
|
||||
- [ ] Today is visually highlighted
|
||||
|
||||
@@ -19,15 +19,29 @@ Default cycle length: 31 days (configurable per user).
|
||||
|
||||
### Cycle Phases
|
||||
|
||||
Based on a 31-day cycle:
|
||||
Phase boundaries scale based on cycle length using a **fixed luteal, variable follicular** approach. The luteal phase (14 days) is biologically consistent; the follicular phase expands or contracts with cycle length.
|
||||
|
||||
**Phase Calculation Formula:**
|
||||
|
||||
Given `cycleLength` (user-configurable, default 31):
|
||||
|
||||
| Phase | Days | Weekly Limit | Training Type |
|
||||
|-------|------|--------------|---------------|
|
||||
| MENSTRUAL | 1-3 | 30 min | Gentle rebounding only |
|
||||
| FOLLICULAR | 4-14 | 120 min | Strength + rebounding |
|
||||
| OVULATION | 15-16 | 80 min | Peak performance |
|
||||
| EARLY_LUTEAL | 17-24 | 100 min | Moderate training |
|
||||
| LATE_LUTEAL | 25-31 | 50 min | Gentle rebounding ONLY |
|
||||
| FOLLICULAR | 4 to (cycleLength - 16) | 120 min | Strength + rebounding |
|
||||
| OVULATION | (cycleLength - 15) to (cycleLength - 14) | 80 min | Peak performance |
|
||||
| EARLY_LUTEAL | (cycleLength - 13) to (cycleLength - 7) | 100 min | Moderate training |
|
||||
| LATE_LUTEAL | (cycleLength - 6) to cycleLength | 50 min | Gentle rebounding ONLY |
|
||||
|
||||
**Examples by Cycle Length:**
|
||||
|
||||
| Phase | 28-day | 31-day | 35-day |
|
||||
|-------|--------|--------|--------|
|
||||
| MENSTRUAL | 1-3 | 1-3 | 1-3 |
|
||||
| FOLLICULAR | 4-12 | 4-15 | 4-19 |
|
||||
| OVULATION | 13-14 | 16-17 | 20-21 |
|
||||
| EARLY_LUTEAL | 15-21 | 18-24 | 22-28 |
|
||||
| LATE_LUTEAL | 22-28 | 25-31 | 29-35 |
|
||||
|
||||
## API Endpoints
|
||||
|
||||
@@ -101,7 +115,18 @@ interface PeriodLog {
|
||||
Users with irregular cycles can adjust:
|
||||
- Default: 31 days
|
||||
- Range: 21-45 days
|
||||
- Affects phase day boundaries proportionally
|
||||
- Affects phase day boundaries per formula above
|
||||
|
||||
## Known Limitations
|
||||
|
||||
The following scenarios are **out of scope** for MVP:
|
||||
|
||||
- **Pregnancy** - Cycle tracking pauses; app not designed for pregnancy
|
||||
- **Menopause** - Irregular/absent cycles not supported
|
||||
- **Hormonal birth control** - May suppress or alter natural cycle phases
|
||||
- **Anovulatory cycles** - App assumes ovulation occurs; no detection for skipped ovulation
|
||||
|
||||
Users in these situations should consult healthcare providers for personalized guidance.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
@@ -115,6 +140,8 @@ Users with irregular cycles can adjust:
|
||||
- [ ] `getCycleDay` returns 1 on period start date
|
||||
- [ ] `getCycleDay` handles cycle rollover correctly
|
||||
- [ ] `getPhase` returns correct phase for each day range
|
||||
- [ ] `getPhase` scales correctly for 28-day cycle
|
||||
- [ ] `getPhase` scales correctly for 35-day cycle
|
||||
- [ ] POST `/api/cycle/period` updates user record
|
||||
- [ ] GET `/api/cycle/current` returns accurate phase info
|
||||
- [ ] Days beyond cycle length default to LATE_LUTEAL
|
||||
|
||||
@@ -77,12 +77,68 @@ Visual cycle overview for the current month.
|
||||
- Period days marked distinctly
|
||||
- Quick navigation to previous/next month
|
||||
|
||||
## UI States
|
||||
|
||||
### Loading States
|
||||
|
||||
Use skeleton loaders during data fetching:
|
||||
- Decision card: Shimmer placeholder for status and reason
|
||||
- Data panel: Skeleton rows for each metric
|
||||
- Mini calendar: Placeholder grid
|
||||
|
||||
### Error Handling
|
||||
|
||||
Toast notifications for errors with actionable feedback:
|
||||
- Network errors: "Unable to fetch data. Retry?"
|
||||
- Garmin sync failed: "Garmin data unavailable. Using last known values."
|
||||
- Save errors: "Failed to save. Try again."
|
||||
|
||||
Toast position: Bottom-right, auto-dismiss after 5 seconds (errors persist until dismissed).
|
||||
|
||||
## Dark Mode
|
||||
|
||||
Auto-detect system preference via CSS `prefers-color-scheme`:
|
||||
- No manual toggle
|
||||
- Respect OS-level dark mode setting
|
||||
- Ensure all phase colors maintain contrast in both modes
|
||||
|
||||
## Onboarding
|
||||
|
||||
First-time users see inline prompts/banners for missing setup:
|
||||
|
||||
| Missing State | Banner Message | Action |
|
||||
|---------------|----------------|--------|
|
||||
| Garmin not connected | "Connect your Garmin to get started" | Link to /settings/garmin |
|
||||
| Last period not set | "Set your last period date for accurate tracking" | Opens date picker |
|
||||
| Notification time not set | "Set your preferred notification time" | Link to /settings |
|
||||
|
||||
Banners appear at top of dashboard, dismissable once action completed.
|
||||
|
||||
## Responsive Design
|
||||
|
||||
Mobile-responsive layout:
|
||||
- Single-column layout on mobile (<768px)
|
||||
- All critical metrics visible without scrolling
|
||||
- Touch-friendly toggle buttons (min 44x44px tap targets)
|
||||
- Collapsible sections for secondary content
|
||||
|
||||
## Accessibility
|
||||
|
||||
Basic accessibility requirements:
|
||||
- Semantic HTML (proper heading hierarchy, landmarks)
|
||||
- Keyboard navigation for all interactive elements
|
||||
- Focus indicators visible
|
||||
- Color contrast minimum 4.5:1 for text
|
||||
- Phase colors supplemented with icons/text (not color-only)
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. User can determine training status within 3 seconds of opening app
|
||||
2. All biometric data visible without scrolling on mobile
|
||||
3. Override toggles accessible but not accidentally activated
|
||||
4. Phase colors consistent across all components
|
||||
5. Dashboard usable with keyboard only
|
||||
6. Loading states display within 100ms of navigation
|
||||
|
||||
## Acceptance Tests
|
||||
|
||||
@@ -92,3 +148,9 @@ Visual cycle overview for the current month.
|
||||
- [ ] Nutrition panel shows correct seeds for cycle day
|
||||
- [ ] Override toggles update user record immediately
|
||||
- [ ] Mini calendar renders current month with phase colors
|
||||
- [ ] Skeleton loaders appear during data fetch
|
||||
- [ ] Toast notifications display on error
|
||||
- [ ] Dark mode activates based on system preference
|
||||
- [ ] Onboarding banner shows when Garmin not connected
|
||||
- [ ] Dashboard renders correctly on mobile viewport
|
||||
- [ ] All interactive elements keyboard-accessible
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Garmin Integration Specification
|
||||
|
||||
> **Required**: Garmin connection is mandatory. PhaseFlow cannot function without biometric data from Garmin. The app's core value proposition is combining cycle phases with real physiological data—phase-only mode is not supported.
|
||||
|
||||
## Job to Be Done
|
||||
|
||||
When I connect my Garmin account, I want the app to automatically sync my biometrics daily, so that training decisions are based on real physiological data.
|
||||
|
||||
@@ -81,9 +81,28 @@ Protected by `CRON_SECRET` header.
|
||||
|
||||
### Timezone Handling
|
||||
|
||||
Users store preferred timezone (e.g., `America/New_York`).
|
||||
Users are batched into hourly timezone windows for efficient scheduling.
|
||||
|
||||
Cron runs every hour. Check if current hour in user's timezone matches their `notificationTime`.
|
||||
**Implementation:**
|
||||
1. Cron job runs 24 times daily (once per hour, on the hour)
|
||||
2. Each run queries users whose `notificationTime` hour matches the current UTC hour adjusted for their timezone
|
||||
3. Users are processed in batches within their timezone window
|
||||
|
||||
**Example:**
|
||||
- User A: timezone `America/New_York`, notificationTime `07:00`
|
||||
- User B: timezone `America/Los_Angeles`, notificationTime `07:00`
|
||||
- User C: timezone `Europe/London`, notificationTime `07:00`
|
||||
|
||||
When cron runs at 12:00 UTC:
|
||||
- User A receives notification (12:00 UTC = 07:00 EST)
|
||||
- User B skipped (12:00 UTC = 04:00 PST)
|
||||
- User C skipped (12:00 UTC = 12:00 GMT)
|
||||
|
||||
**Query Logic:**
|
||||
```sql
|
||||
SELECT * FROM users
|
||||
WHERE EXTRACT(HOUR FROM NOW() AT TIME ZONE timezone) = CAST(SUBSTRING(notificationTime, 1, 2) AS INT)
|
||||
```
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
|
||||
138
specs/observability.md
Normal file
138
specs/observability.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# Observability Specification
|
||||
|
||||
## Job to Be Done
|
||||
|
||||
When the app is running in production, I want visibility into its health and behavior, so that I can detect and diagnose issues quickly.
|
||||
|
||||
## Health Check
|
||||
|
||||
### GET `/api/health`
|
||||
|
||||
Returns application health status for monitoring and load balancer checks.
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"timestamp": "2024-01-10T12:00:00Z",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (503 Service Unavailable):**
|
||||
```json
|
||||
{
|
||||
"status": "unhealthy",
|
||||
"timestamp": "2024-01-10T12:00:00Z",
|
||||
"error": "PocketBase connection failed"
|
||||
}
|
||||
```
|
||||
|
||||
**Checks Performed:**
|
||||
- PocketBase connectivity
|
||||
- Basic app startup complete
|
||||
|
||||
**Usage:**
|
||||
- Nomad health checks
|
||||
- Uptime monitoring (e.g., Uptime Kuma)
|
||||
- Load balancer health probes
|
||||
|
||||
## Prometheus Metrics
|
||||
|
||||
### GET `/metrics`
|
||||
|
||||
Returns Prometheus-format metrics for scraping.
|
||||
|
||||
**Standard Node.js Metrics:**
|
||||
- `nodejs_heap_size_total_bytes`
|
||||
- `nodejs_heap_size_used_bytes`
|
||||
- `nodejs_eventloop_lag_seconds`
|
||||
- `http_request_duration_seconds` (histogram)
|
||||
- `http_requests_total` (counter)
|
||||
|
||||
**Custom Application Metrics:**
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `phaseflow_garmin_sync_total` | counter | `status` (success/failure) | Garmin sync attempts |
|
||||
| `phaseflow_garmin_sync_duration_seconds` | histogram | - | Garmin sync duration |
|
||||
| `phaseflow_email_sent_total` | counter | `type` (daily/warning) | Emails sent |
|
||||
| `phaseflow_decision_engine_calls_total` | counter | `decision` (REST/GENTLE/...) | Decision engine invocations |
|
||||
| `phaseflow_active_users` | gauge | - | Users with activity in last 24h |
|
||||
|
||||
**Implementation:**
|
||||
Use `prom-client` npm package for metrics collection.
|
||||
|
||||
## Structured Logging
|
||||
|
||||
### Format
|
||||
|
||||
JSON-structured logs for all significant events:
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2024-01-10T12:00:00.000Z",
|
||||
"level": "info",
|
||||
"message": "Garmin sync completed",
|
||||
"userId": "user123",
|
||||
"duration_ms": 1250,
|
||||
"metrics": {
|
||||
"bodyBattery": 95,
|
||||
"hrvStatus": "Balanced"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Log Levels
|
||||
|
||||
| Level | Usage |
|
||||
|-------|-------|
|
||||
| `error` | Failures requiring attention (sync failures, email errors) |
|
||||
| `warn` | Degraded behavior (using cached data, retries) |
|
||||
| `info` | Normal operations (sync complete, email sent, decision made) |
|
||||
|
||||
### Key Events to Log
|
||||
|
||||
| Event | Level | Fields |
|
||||
|-------|-------|--------|
|
||||
| Auth success | info | userId |
|
||||
| Auth failure | warn | reason, ip |
|
||||
| Garmin sync start | info | userId |
|
||||
| Garmin sync complete | info | userId, duration_ms, metrics |
|
||||
| Garmin sync failure | error | userId, error, attempt |
|
||||
| Email sent | info | userId, type, recipient |
|
||||
| Email failed | error | userId, type, error |
|
||||
| Decision calculated | info | userId, decision, reason |
|
||||
| Period logged | info | userId, date |
|
||||
| Override toggled | info | userId, override, enabled |
|
||||
|
||||
### Implementation
|
||||
|
||||
Use structured logger (e.g., `pino`) configured for JSON output:
|
||||
|
||||
```typescript
|
||||
import pino from 'pino';
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
formatters: {
|
||||
level: (label) => ({ level: label }),
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Success Criteria
|
||||
|
||||
1. Health endpoint responds in under 100ms
|
||||
2. Metrics endpoint scrapable by Prometheus
|
||||
3. All key events logged with consistent structure
|
||||
4. Logs parseable by log aggregators (Loki, ELK, etc.)
|
||||
|
||||
## Acceptance Tests
|
||||
|
||||
- [ ] GET `/api/health` returns 200 when healthy
|
||||
- [ ] GET `/api/health` returns 503 when PocketBase unreachable
|
||||
- [ ] GET `/metrics` returns valid Prometheus format
|
||||
- [ ] Custom metrics increment on relevant events
|
||||
- [ ] Logs output valid JSON to stdout
|
||||
- [ ] Error logs include stack traces
|
||||
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