test-pyramid
Use this skill whenever writing unit tests, integration tests, E2E tests, or simulator-specific tests for the Myelin platform. This defines what belongs at each layer of the test pyramid, coverage tar
SKILL.mdprimary_build_source/docs/skills/4/mnt/user-data/outputs/test-pyramid/SKILL.md
GitHubThis skill defines the test pyramid for the Myelin platform. Tests are the executable specification of the platform's correctness. The pyramid structure prevents slow, brittle E2E tests from doing the work that fast unit tests should do — and prevents unit tests from missing the integration failures that only surface when real systems talk to each other.
Pyramid Layers
[ E2E — Learner & Lecturer Journeys ]
[ Integration — API + DB + Queue + Storage ]
[ Unit — Domain Logic, Scoring, Policy ]
[ Simulator-Specific — Sequence Gating, Events, Budget Tests ]
Target distribution: 60% unit, 25% integration, 10% E2E, 5% simulator-specific.
Layer 1 — Unit Tests
What belongs here
Domain logic with no external dependencies. Every function that makes a decision must have unit tests. These run in milliseconds — there is no excuse for skipping them.
Mandatory unit test subjects
Simulation scoring and gating:
validateRotationAngle(angle)— pass at exactlyπ/2 ± 0.05, fail outsidevalidateHaspDistance(distance)— pass at < 0.02m, fail at >= 0.02mvalidatePressure(bar)— pass at < 0.1, fail at >= 0.1resolveTorquePhase(angle)— returns correct phase torque from[8.2, 3.1, 11.8]array- All 8
dispatch()action handlers in the simulation state machine SESSION_COMPLETEguard — must require all 7 step pass flags true; must reject partial completion
State machine transitions:
- Every valid transition in the simulation state machine
- Every invalid transition — must reject and not mutate state
- Wrong-order interaction — must not change state, must return feedback only
- Session reset — must return all step states to LOCKED/UNLOCKED as per scenario config
Authentication and authorisation:
requireRole()— pass for correct roles, reject for all others, including adjacent rolesrequireTenantScope()— pass for matching tenant, reject for mismatched tenantverifyAccessToken()— valid token passes, expired token rejects, tampered token rejects, revoked jti rejects
Scoring and KPI formulas:
- Module completion rate formula with known numerator/denominator
- First-pass rate formula
- Median time-to-competency with known dataset
- Baseline freeze — must reject second freeze call on same cohort
Agent context and safety:
assembleAgentContext()— verify all required fields present, correct fallbacks when simulation not activerecentErrorstrimming — must return last 3 only when more than 3 existclassifyAgentResponse()— each of the 8 response classes with representative inputs- Safety guardrail trigger detection — assessment boundary, distress keywords, out-of-scope
POPIA and consent:
requireConsent()middleware — must block when consent absent, must block when version mismatchresolveChannels()— mandatory notifications bypass all prefs; SMS requires explicit opt-inresolveScheduledTime()— must defer to after quiet hours window
// Example: SESSION_COMPLETE guard
describe('SESSION_COMPLETE guard', () => {
it('fires when all 7 steps are PASSED', () => {
const state = buildAllPassedState();
const result = dispatch(state, { type: 'SESSION_COMPLETE' });
expect(result.sessionStatus).toBe('COMPLETED');
});
it('rejects when any step is not PASSED', () => {
const state = buildStateWithStep6NotPassed();
expect(() => dispatch(state, { type: 'SESSION_COMPLETE' }))
.toThrow('SESSION_COMPLETE_GUARD_FAILED');
});
});
Layer 2 — Integration Tests
What belongs here
Behaviour that crosses system boundaries: API endpoints that write to the database, queue jobs that trigger downstream effects, storage operations.
Use a real test database (not mocks) seeded with fixtures. Use a real queue (in-memory or test container). Do not mock the database in integration tests — mocks hide the real failure modes.
Mandatory integration test subjects
API + Database:
- Learner registration flow: POST
/auth/register→ user created in DB → consent record absent → consent gate blocks training access - Simulation session creation: POST
/sim/session→ session record created → demoMode flag set correctly → immutable after creation - Step event ingestion: POST
/sim/event→ event written to correct table (live vs demo) → state machine advances - Role assignment: POST
/admin/roles→ role record created → audit log entry written in same transaction - Cross-tenant rejection: attempt to read another tenant's submission → 403 returned, no data leaked, audit entry written as SECURITY_TENANT_SCOPE_VIOLATION
Queue + Storage:
- File upload: POST
/upload→ malware scan queued → transcode job queued → object key is system-generated (never original filename) → signed URL expires at 15 minutes - Notification dispatch: trigger
RESULT_STATEMENT_ISSUED→ notification record inserted as QUEUED → job enqueued →MENTOR_INVITE(mandatory) cannot be suppressed by recipient prefs
Auth lifecycle:
- Login: POST
/auth/login→ valid credentials → access + refresh token returned → tokens verifiable → jti stored - Token refresh: POST
/auth/refresh→ valid refresh token → new access token returned → old access token still valid until TTL - Session revocation: POST
/auth/revoke→ jti added to revocation list → subsequent use of revoked token returns 401
Layer 3 — E2E Tests
What belongs here
Critical user journeys tested against a running staging environment. These are slow — run them before every release, not on every commit.
Mandatory E2E journeys
Learner journey:
- Register → land on consent gate → confirm consent → enrol in programme
- Complete module content → enter simulator → complete all 7 LOTO steps → reach SESSION_COMPLETE
- Submit evidence → receive UNDER_REVIEW status → receive notification when outcome set
- Receive PASSED result → result statement generated → verification URL resolves
Lecturer journey:
- Login as Lecturer → view cohort dashboard → see pending submission
- Open submission → score rubric criteria → set outcome PASSED → learner notified
- View KPI dashboard → completion rate visible → no data from other tenants visible
Admin journey:
- Login as Admin → invite mentor → mentor accepts invite → mentor assigned to placement
- Trigger data subject access request → export pack contains all required entity types
- View audit log → actions from own tenant visible → actions from other tenants not visible
Security rejection journey:
- Attempt unauthenticated API call → 401 returned
- Attempt cross-tenant data access with valid token → 403 returned
- Attempt file upload with executable MIME type → rejected with FILE_TYPE_NOT_ALLOWED
Layer 4 — Simulator-Specific Tests
What belongs here
Tests unique to the 3D simulation that cannot be exercised at the unit or integration layer because they depend on the physics engine, timing, or rendering context.
Mandatory simulator-specific tests
Step sequence gating:
- Attempting to interact with Step 3 (hasp) before Step 2 (valve) is PASSED → wrong-order feedback returned, no state change
- Completing steps out of order in all 6 invalid permutations → no false PASS events
- Attempting SESSION_COMPLETE interaction without all steps PASSED → guard fires, session not completed
Event emission:
- Every
SimulationStepEventPASS and FAIL for all 7 steps must be emitted with the correctmeasuredValueandrequiredThreshold SimCompleteEventemitted exactly once per session at SESSION_COMPLETE — never twiceSimPerfEventemitted every 60 seconds of active session time — not during tab-hidden time- Demo mode: all events written to
demo_simulation_step_events, none tosimulation_step_events
Physics validation:
- Rotation validation:
π/2 ± 0.05 radthreshold — test at exactly the boundary values - Hasp distance: test at 0.019m (pass), 0.020m (fail boundary), 0.021m (fail)
- Pressure model: test at 0.09 bar (pass), 0.10 bar (fail boundary), 0.11 bar (fail)
- Torque curve: dispatching motor force at each of the 3 phase boundaries returns correct resistance value
Performance budget:
- Simulate 60-second LOTO traversal with all assets loaded at HD quality tier
- Assert: p95 frame time < 22ms (45+ fps equivalent)
- Assert: physics step time avg < 3ms
- Assert: texture memory < 450MB (90% of ceiling)
- Assert: no
requestAnimationFramemisses during step interaction events
describe('Hasp distance threshold', () => {
it('PASS at 0.019m', () => {
expect(validateHaspDistance(0.019)).toBe('PASS');
});
it('FAIL at exactly 0.020m', () => {
expect(validateHaspDistance(0.020)).toBe('FAIL');
});
it('FAIL at 0.025m', () => {
expect(validateHaspDistance(0.025)).toBe('FAIL');
});
});
Test Data Requirements
- Unit tests: use pure in-memory fixtures — no database, no network
- Integration tests: dedicated test database, reset between test runs, seeded with known fixture datasets per test suite
- E2E tests: isolated staging environment, dedicated test tenant, synthetic learner accounts
- Never use production data in any test environment
- Fixture learner IDs must be in a reserved namespace (e.g.
test-*) to prevent accidental inclusion in production reporting
CI/CD Integration
| Layer | When it runs | Must pass to |
|---|---|---|
| Unit | Every commit, every PR | Merge to main |
| Integration | Every PR merge to main | Deploy to staging |
| E2E | Every staging deployment | Promote to production |
| Simulator-specific | Every staging deployment | Promote to production |
A failing unit test must not be merged. A failing integration or E2E test must not be promoted to production. Skipped tests are treated as failures in the CI pipeline — do not skip tests to make a build pass.
What Not To Do
- Do not mock the database in integration tests — use a real test database
- Do not use production data in test environments
- Do not skip tests to make a build pass — fix the test or fix the code
- Do not put integration-level assertions (DB state, queue jobs) in unit tests
- Do not run E2E tests on every commit — they are pre-release only
- Do not allow
SESSION_COMPLETEguard tests to test only the happy path — all 7 step failure permutations must be covered - Do not test boundary values only at round numbers — always test at the exact threshold value (0.020m, not 0.02m rounded)