Phase 4 of the edge functions testing plan has been successfully completed. Database schema contract tests have been implemented to catch schema drift issues before they cause production failures.
Result: ✅ 86/86 tests passing (100% success rate, +19 tests from Phase 4)
File Created: supabase/functions/_shared/schema-contracts.test.ts (19 tests)
Purpose: Verify that data transformers produce records matching the actual database schema. This prevents silent failures when code references columns that don't exist or have been renamed.
- ✅ Has
device_id(nottracker_id) - REGRESSION TEST - ✅ Has all required fields (
device_id,activity_date,vendor_metadata) - ✅ Allows null for optional numeric fields (
steps,distance_m, etc.)
Why This Matters:
- The
tracker_id → device_idrename was a production bug - Ensures transformers produce valid records for database inserts
- Validates that 0 is preserved (not converted to null)
- ✅ Has
device_id(nottracker_id) - REGRESSION TEST - ✅ Has all required fields (
device_id,start_time,end_time,total_duration_minutes,vendor_metadata) - ✅ Handles naps (short duration sleep sessions)
Why This Matters:
- Sleep sessions can be naps (< 3 hours) or main sleep
- Timestamps must be ISO 8601 format
- Duration in minutes (not seconds)
- ✅ Has
device_id(nottracker_id) - REGRESSION TEST - ✅ Has all required fields (
device_id,activity_type,start_time,vendor_metadata) - ✅ Allows null for optional fields (
duration_sec,distance_m,calories_burned)
Why This Matters:
- Some activities (like Yoga) don't have distance
- 0 calories is valid (different from null)
activity_typemust be a non-empty string
- ✅ Has
device_id(nottracker_id) - REGRESSION TEST - ✅ Has all required fields (
device_id,timestamp) - ✅ Allows null for metrics (HR-only or steps-only data points)
Why This Matters:
- Some data points only have heart rate OR steps
- Timestamps must be precise (minute-level granularity)
- Allows for sparse data (null is valid for missing metrics)
- ✅ All fitness tables use
device_idconsistently - REGRESSION TEST - ✅ All fitness tables have
vendor_metadatafor extensibility
Why This Matters:
- Ensures consistency across all fitness tracking tables
vendor_metadataallows provider-specific data without schema changes- Catches regressions where one table reverts to old column names
- ✅ Timestamps are ISO 8601 strings (not Date objects)
- ✅ Dates are YYYY-MM-DD strings (not full timestamps)
- ✅ Numeric fields accept 0 as valid value (not null)
Why This Matters:
- Database expects
timestamptzas ISO 8601 strings activity_dateis date-only (no time component)- CRITICAL: 0 ≠ null (0 = "measured as zero", null = "no data")
- ✅
vendor_metadatastructure is documented for each provider - ✅ Schema uses consistent naming conventions
Why This Matters:
- Documents expected
vendor_metadatafields for WHOOP vs Fitbit - Validates naming conventions (FK ends with
_id, timestamps end with_time, etc.) - Ensures future schema changes maintain consistency
Impact: Production bug where code referenced old column name
Fix: REGRESSION tests in all 4 table contracts
Protection: 4 tests ensure we never revert to tracker_id
Impact: 0 calories/steps/distance would show as "no data" Fix: Explicit tests that 0 is preserved (not converted to null) Protection: 1 test validates 0 vs null semantics across all numeric fields
Impact: Database expects ISO 8601 strings, not Date objects Fix: Tests validate string format with 'T' separator and 'Z' UTC indicator Protection: 2 tests ensure correct timestamp and date formats
Impact: Database inserts fail if required fields are missing Fix: Tests validate all required fields are present Protection: 4 tests (one per table) check required field presence
validateRequiredFields()
- Ensures all required fields exist in a record
- Throws descriptive error if missing
- Used in all contract tests
validateForbiddenFields()
- Ensures renamed/removed fields are NOT present
- Catches regressions where old column names creep back in
- Critical for
tracker_id → device_idmigration
cd supabase/functions
./run-tests.shcd supabase/functions
deno test --allow-env --allow-net _shared/schema-contracts.test.tsdeno test --allow-env --allow-net --watch _shared/schema-contracts.test.tsrun-tests.sh now includes contract tests:
Test Execution Order:
- Orchestrator regression tests (12 tests)
- WHOOP client integration tests (5 tests)
- Fitbit client integration tests (6 tests)
- Fitbit transformer unit tests (28 tests)
- Schema contract tests (19 tests) ← NEW
- WHOOP transformer unit tests (16 tests)
Total: 86 tests, ~184ms runtime
Contract tests verify structure (schema), not behavior (logic):
❌ Not Tested by Contract Tests:
- Unit conversions (kJ → kcal, ms → seconds)
- Data transformations (API response → database record)
- Business logic (deduplication, nap detection, sport mapping)
- API calls or token refresh
✅ Already Tested by Other Phases:
- Phase 1 (Transformer Unit Tests) - Covers all transformations and conversions
- Phase 2 (Integration Tests) - Covers API clients and retry logic
- Phase 3 (Orchestrator Regression Tests) - Covers function-to-function auth
Contract tests complement, not replace, existing tests.
// ❌ WRONG - Will fail contract test
const record = {
tracker_id: "device-123", // OLD COLUMN NAME
activity_date: "2026-02-03",
steps: 10000,
};
// ✅ CORRECT - Passes contract test
const record = {
device_id: "device-123", // NEW COLUMN NAME
activity_date: "2026-02-03",
steps: 10000,
};// ❌ WRONG - Missing vendor_metadata
const record = {
device_id: "device-123",
activity_date: "2026-02-03",
steps: 10000,
// Missing vendor_metadata - database insert will fail
};
// ✅ CORRECT - Includes vendor_metadata
const record = {
device_id: "device-123",
activity_date: "2026-02-03",
steps: 10000,
vendor_metadata: {}, // Required, even if empty
};// ❌ WRONG - Date object
const record = {
device_id: "device-123",
start_time: new Date("2026-02-03T08:00:00.000Z"), // Date object
};
// ✅ CORRECT - ISO 8601 string
const record = {
device_id: "device-123",
start_time: "2026-02-03T08:00:00.000Z", // String
};// ❌ WRONG - Loses zero values
const calories = record.kilojoule ? Math.round(record.kilojoule * 0.239) : null;
// If kilojoule is 0, this returns null (data loss)
// ✅ CORRECT - Preserves zero values
const calories = record.kilojoule != null ? Math.round(record.kilojoule * 0.239) : null;
// If kilojoule is 0, this returns 0 (correct)Contract tests validate these naming conventions:
-
Foreign Keys: End with
_id- ✅
device_id,user_id,cycle_id - ❌
device,tracker
- ✅
-
Timestamps: End with
_timeor_at- ✅
start_time,end_time,created_at - ❌
start,end,created
- ✅
-
Durations: End with
_minutesor_sec- ✅
total_duration_minutes,duration_sec - ❌
duration,time
- ✅
-
Distances: End with
_m(meters)- ✅
distance_m - ❌
distance,distance_km
- ✅
-
Dates: End with
_date- ✅
activity_date - ❌
date,day
- ✅
- Runtime: ~10ms for all 19 contract tests
- Fast enough for: Pre-commit hooks, CI/CD, watch mode
- No external dependencies: All tests are pure validation
Not Yet Implemented:
- E2E tests with real database connection
- Foreign key constraint validation (device_id references connected_devices)
- Index existence checks (performance)
- RLS policy validation (security)
Why Not Yet:
- Requires test database setup
- Requires Supabase client initialization
- Higher complexity, slower execution
Current contract tests are sufficient for catching schema drift without database access.
Contract tests are included in:
- ✅
run-tests.sh- Local test runner - ✅
pre-deploy.sh- Pre-deployment safety check - ⏸️ GitHub Actions workflow (when activated)
No additional setup required - contract tests run automatically.
- Testing Plan:
/docs/edge-functions-testing-plan.md- Full 4-phase plan - Phase 1 Complete:
/docs/phase-1-transformers-complete.md- Transformer tests (44 tests) - Phase 2 Complete:
/docs/phase-2-tests-fixed.md- Integration tests (23 tests) - Phase 4 Complete: This document - Contract tests (19 tests)
- Quick Reference:
/supabase/functions/TESTING.md- Commands and examples - CI/CD Guide:
/ci-cd/EDGE-FUNCTION-TESTS.md- GitHub Actions setup
| Metric | Target | Actual | Status |
|---|---|---|---|
| Test Pass Rate | 100% | 100% (86/86) | ✅ |
| Contract Coverage | All tables | 4/4 tables | ✅ |
| Runtime | < 500ms | ~184ms | ✅ |
| Flaky Tests | 0 | 0 | ✅ |
| Regressions Prevented | N/A | 4 (tracker_id, 0 values, timestamps, required fields) | ✅ |
| Phase | Status | Tests |
|---|---|---|
| Phase 1: Transformer Unit Tests | ✅ Complete | 44 tests |
| Phase 2: Integration Tests | ✅ Complete | 23 tests |
| Phase 3: Orchestrator Regression Tests | ✅ Complete | (included in Phase 2) |
| Phase 4: Contract Tests | ✅ Complete | 19 tests |
| Phase 5: E2E Tests with Test DB | ⏸️ TODO | 0 tests |
Total Implemented: 86 tests (Phases 1-4) Remaining: Phase 5 (E2E with database)
Phase 4 is now complete with:
- ✅ 86 tests passing (23 from Phase 2 + 44 from Phase 1 + 19 from Phase 4)
- ✅ All 4 fitness tables have contract tests
- ✅ Fast execution (~184ms for all tests)
- ✅ 4 critical bugs prevented (schema drift, zero values, timestamps, required fields)
- ✅ Ready for production use
The contract tests provide a critical safety net:
- Preventing schema drift when columns are renamed or removed
- Validating data types (strings vs Date objects, 0 vs null)
- Ensuring required fields are never omitted
- Documenting schema contracts through executable tests
Phases 1-4 combined: 86 tests, 100% reliable, ready for CI/CD! 🎉
Last Updated: February 3, 2026 Status: ✅ PHASE 4 COMPLETE Next Phase: Phase 5 (E2E tests with test database) - Optional, not critical