docs: separate documentation and specs into initial commit

Establish baseline for project documentation including BMAD specs, PRD, and system architecture notes.
This commit is contained in:
2026-02-11 11:49:49 +08:00
parent 8c87d71aa2
commit e9897388dc
34 changed files with 11920 additions and 8777 deletions

View File

@@ -0,0 +1,513 @@
# Story 1.17-a: Add Load Testing for NFR4 (100 Concurrent Users)
**Status:** Draft
**Epic:** Epic 1 - Webflow to Payload CMS + Astro Migration
**Priority:** P1 (High - NFR4 Validation Required)
**Estimated Time:** 3 hours
## Story
**As a** Development Team,
**I want** a load testing framework that validates system performance under concurrent user load,
**So that** we can ensure NFR4 compliance (100 concurrent users) before production deployment.
## Context
This is a Sprint 1 addition story. NFR4 requires the system to support at least 100 concurrent users without performance degradation. Load testing was only implied in the original plan and needs explicit validation.
**Story Source:**
- NFR4 from `docs/prd/02-requirements.md`
- Sprint 1 adjustments in `sprint-status.yaml`
- Task specs from `docs/prd/epic-1-stories-1.3-1.17-tasks.md`
**NFR4 Requirement:**
> "The system must support at least 100 concurrent users without performance degradation."
## Acceptance Criteria
### Part 1: Load Testing Framework
1. **AC1 - Tool Selected**: k6 or Artillery chosen and installed
2. **AC2 - Test Scripts Created**: Scripts for public browsing and admin operations
### Part 2: Test Scenarios
3. **AC3 - Public Browsing Test**: 100 concurrent users browsing pages
4. **AC4 - Admin Operations Test**: 20 concurrent admin users
5. **AC5 - API Performance Test**: Payload CMS API endpoints under load
### Part 3: Performance Targets
6. **AC6 - Response Time**: 95th percentile response time < 500ms
7. **AC7 - Error Rate**: Error rate < 1%
8. **AC8 - Cloudflare Limits**: Validated within Workers limits
### Part 4: Reporting
9. **AC9 - Test Report**: Generated report with results and recommendations
10. **AC10 - CI Integration**: Tests can be run on demand
## Dev Technical Guidance
### Task 1: Select and Install Load Testing Tool
**Recommended: k6** (Grafana's load testing tool)
**Installation:**
```bash
# Install k6
pnpm add -D k6
# Or globally
brew install k6 # macOS
```
**Why k6:**
- JavaScript-based tests (familiar to devs)
- Good Cloudflare Workers support
- Excellent reporting and metrics
- Easy CI/CD integration
### Task 2: Create Load Test Scripts
**File:** `apps/frontend/tests/load/public-browsing.js`
```javascript
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate } from 'k6/metrics'
// Custom metrics
const errorRate = new Rate('errors')
// Test configuration
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up to 20 users
{ duration: '1m', target: 50 }, // Ramp up to 50 users
{ duration: '2m', target: 100 }, // Ramp up to 100 users (NFR4 target)
{ duration: '2m', target: 100 }, // Stay at 100 users
{ duration: '30s', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
errors: ['rate<0.01'], // Error rate < 1%
http_req_failed: ['rate<0.01'], // Failed requests < 1%
},
}
const BASE_URL = __ENV.BASE_URL || 'http://localhost:4321'
// Pages to test
const pages = [
'/',
'/about',
'/solutions',
'/contact',
'/blog',
'/portfolio',
]
export function setup() {
// Optional: Login and get auth token for admin tests
const loginRes = http.post(`${BASE_URL}/api/login`, JSON.stringify({
email: 'test@example.com',
password: 'test123',
}), {
headers: { 'Content-Type': 'application/json' },
})
if (loginRes.status === 200) {
return { token: loginRes.json('token') }
}
return {}
}
export default function(data) {
// Pick a random page
const page = pages[Math.floor(Math.random() * pages.length)]
// Make request
const res = http.get(`${BASE_URL}${page}`, {
tags: { name: `Page: ${page}` },
})
// Check response
const success = check(res, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
'page contains content': (r) => r.html().find('body').length > 0,
})
errorRate.add(!success)
// Think time between requests (2-8 seconds)
sleep(Math.random() * 6 + 2)
}
export function teardown(data) {
console.log('Load test completed')
}
```
**File:** `apps/frontend/tests/load/admin-operations.js`
```javascript
import http from 'k6/http'
import { check, sleep } from 'k6'
import { Rate } from 'k6/metrics'
const errorRate = new Rate('errors')
export const options = {
stages: [
{ duration: '20s', target: 5 }, // Ramp up to 5 admin users
{ duration: '1m', target: 10 }, // Ramp up to 10 admin users
{ duration: '2m', target: 20 }, // Ramp up to 20 admin users
{ duration: '1m', target: 20 }, // Stay at 20 users
{ duration: '20s', target: 0 }, // Ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'],
errors: ['rate<0.01'],
},
}
const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'
const API_URL = __ENV.API_URL || 'http://localhost:3000/api'
export function setup() {
// Login as admin
const loginRes = http.post(`${API_URL}/users/login`, JSON.stringify({
email: 'admin@enchun.tw',
password: 'admin123',
}), {
headers: { 'Content-Type': 'application/json' },
})
if (loginRes.status !== 200) {
throw new Error('Login failed')
}
return { token: loginRes.json('token') }
}
export default function(data) {
const headers = {
'Authorization': `JWT ${data.token}`,
'Content-Type': 'application/json',
}
// Test different admin operations
const operations = [
// List posts
() => {
const res = http.get(`${API_URL}/posts?limit=10`, { headers })
check(res, { 'posts list status': (r) => r.status === 200 })
},
// List categories
() => {
const res = http.get(`${API_URL}/categories?limit=20`, { headers })
check(res, { 'categories list status': (r) => r.status === 200 })
},
// Get globals
() => {
const res = http.get(`${API_URL}/globals/header`, { headers })
check(res, { 'globals status': (r) => r.status === 200 })
},
// Get media
() => {
const res = http.get(`${API_URL}/media?limit=10`, { headers })
check(res, { 'media list status': (r) => r.status === 200 })
},
]
// Execute random operation
const operation = operations[Math.floor(Math.random() * operations.length)]
const res = operation()
errorRate.add(!res)
// Think time
sleep(Math.random() * 3 + 1)
}
```
**File:** `apps/backend/tests/load/api-performance.js`
```javascript
import http from 'k6/http'
import { check } from 'k6'
import { Rate } from 'k6/metrics'
const errorRate = new Rate('errors')
export const options = {
scenarios: {
concurrent_readers: {
executor: 'constant-vus',
vus: 50,
duration: '2m',
gracefulStop: '30s',
},
concurrent_writers: {
executor: 'constant-vus',
vus: 10,
duration: '2m',
startTime: '30s',
gracefulStop: '30s',
},
},
thresholds: {
http_req_duration: ['p(95)<500'],
errors: ['rate<0.01'],
},
}
const API_URL = __ENV.API_URL || 'http://localhost:3000/api'
export function setup() {
// Create test user for write operations
const loginRes = http.post(`${API_URL}/users/login`, JSON.stringify({
email: 'test-writer@example.com',
password: 'test123',
}), {
headers: { 'Content-Type': 'application/json' },
})
return { token: loginRes.json('token') || '' }
}
export default function(data) {
const headers = {
'Content-Type': 'application/json',
}
if (data.token) {
headers['Authorization'] = `JWT ${data.token}`
}
// Mix of read operations
const endpoints = [
() => http.get(`${API_URL}/posts?limit=10&depth=1`, { headers }),
() => http.get(`${API_URL}/categories`, { headers }),
() => http.get(`${API_URL}/globals/header`, { headers }),
() => http.get(`${API_URL}/globals/footer`, { headers }),
]
const res = endpoints[Math.floor(Math.random() * endpoints.length)]()
check(res, {
'status 200': (r) => r.status === 200,
'response < 500ms': (r) => r.timings.duration < 500,
})
errorRate.add(res.status !== 200)
}
```
### Task 3: Create Test Runner Scripts
**File:** `package.json` scripts
```json
{
"scripts": {
"test:load": "npm-run-all -p test:load:public test:load:admin",
"test:load:public": "k6 run apps/frontend/tests/load/public-browsing.js",
"test:load:admin": "k6 run apps/frontend/tests/load/admin-operations.js",
"test:load:api": "k6 run apps/backend/tests/load/api-performance.js",
"test:load:report": "k6 run --out json=load-test-results.json apps/frontend/tests/load/public-browsing.js"
}
}
```
### Task 4: Cloudflare Workers Limits Validation
**Create documentation file:** `docs/load-testing-cloudflare-limits.md`
```markdown
# Cloudflare Workers Limits for Load Testing
## Relevant Limits
| Resource | Limit | Notes |
|----------|-------|-------|
| CPU Time | 10ms (Free), 50ms (Paid) | Per request |
| Request Timeout | 30 seconds | Wall-clock time |
| Concurrent Requests | No limit | But fair use applies |
| Workers Requests | 100,000/day (Free) | Paid plan has more |
## Testing Strategy
1. Monitor CPU time per request during load tests
2. Ensure 95th percentile < 50ms CPU time
3. Use `wrangler dev` local mode for initial testing
4. Test on preview deployment before production
## Monitoring
Use Cloudflare Analytics to verify:
- Request count by endpoint
- CPU usage percentage
- Cache hit rate
- Error rate
```
### File Structure
```
apps/
├── frontend/
│ └── tests/
│ └── load/
│ ├── public-browsing.js ← CREATE
│ └── admin-operations.js ← CREATE
├── backend/
│ └── tests/
│ └── load/
│ └── api-performance.js ← CREATE
docs/
└── load-testing-cloudflare-limits.md ← CREATE
package.json ← MODIFY (add scripts)
```
## Tasks / Subtasks
### Part 1: Setup
- [ ] **Task 1.1**: Install k6
- [ ] Add k6 as dev dependency
- [ ] Verify installation
- [ ] Add npm scripts
- [ ] **Task 1.2**: Create test directories
- [ ] Create apps/frontend/tests/load
- [ ] Create apps/backend/tests/load
### Part 2: Public Browsing Test
- [ ] **Task 2.1**: Create public-browsing.js
- [ ] Implement 100 user ramp-up
- [ ] Add page navigation logic
- [ ] Configure thresholds
- [ ] **Task 2.2**: Run initial test
- [ ] Test against local dev server
- [ ] Verify results
- [ ] Adjust think times if needed
### Part 3: Admin Operations Test
- [ ] **Task 3.1**: Create admin-operations.js
- [ ] Implement auth flow
- [ ] Add admin operations
- [ ] Configure 20 user target
- [ ] **Task 3.2**: Run admin test
- [ ] Verify auth works
- [ ] Check performance metrics
### Part 4: API Performance Test
- [ ] **Task 4.1**: Create api-performance.js
- [ ] Implement concurrent readers/writers
- [ ] Add all key endpoints
- [ ] **Task 4.2**: Run API test
- [ ] Verify API performance
- [ ] Check for bottlenecks
### Part 5: Cloudflare Validation
- [ ] **Task 5.1**: Create limits documentation
- [ ] **Task 5.2**: Test on Workers environment
- [ ] **Task 5.3**: Verify CPU time limits
### Part 6: Reporting
- [ ] **Task 6.1**: Generate test report
- [ ] Run all tests
- [ ] Generate HTML report
- [ ] Document results
- [ ] **Task 6.2**: Create recommendations
- [ ] Identify bottlenecks
- [ ] Suggest optimizations
- [ ] Document for deployment
## Testing Requirements
### Performance Targets
| Metric | Target | Threshold |
|--------|--------|-----------|
| Response Time (p95) | < 500ms | NFR5 |
| Error Rate | < 1% | Acceptable |
| Concurrent Users | 100 | NFR4 |
| Admin Users | 20 | Target |
### Test Scenarios
1. **Public Browsing** (100 users):
- Homepage
- About page
- Solutions page
- Contact page
- Blog listing
- Portfolio listing
2. **Admin Operations** (20 users):
- List posts
- List categories
- Get globals (Header/Footer)
- List media
3. **API Performance** (50 readers + 10 writers):
- GET /api/posts
- GET /api/categories
- GET /api/globals/*
- Mixed read/write operations
### Manual Testing Checklist
- [ ] k6 installed successfully
- [ ] Public browsing test runs
- [ ] Admin operations test runs
- [ ] API performance test runs
- [ ] All tests pass thresholds
- [ ] Results documented
- [ ] Cloudflare limits validated
- [ ] Recommendations created
## Risk Assessment
| Risk | Probability | Impact | Mitigation |
|------|------------|--------|------------|
| Local testing not representative | Medium | Medium | Test on preview deployment |
| Cloudflare limits exceeded | Low | High | Monitor CPU time, optimize |
| Test data pollution | Medium | Low | Use test environment |
| False failures | Low | Low | Calibrate thresholds properly |
## Definition of Done
- [ ] k6 installed and configured
- [ ] Public browsing test script created
- [ ] Admin operations test script created
- [ ] API performance test script created
- [ ] All tests run successfully
- [ ] Performance targets met (p95 < 500ms, errors < 1%)
- [ ] 100 concurrent user test passed
- [ ] Cloudflare Workers limits validated
- [ ] Test report generated
- [ ] Recommendations documented
- [ ] sprint-status.yaml updated
## Dev Agent Record
### Agent Model Used
*To be filled by Dev Agent*
### Debug Log References
*To be filled by Dev Agent*
### Completion Notes
*To be filled by Dev Agent*
### File List
*To be filled by Dev Agent*
## Change Log
| Date | Action | Author |
|------|--------|--------|
| 2026-01-31 | Story created (Draft) | SM Agent (Bob) |