Apply comprehensive enhancements to iOS prefetch plugin testing and validation system per directive requirements. Technical Correctness Improvements: - Enhanced BGTask scheduling with validation (60s minimum lead time) - Implemented one active task rule (cancel existing before scheduling) - Added graceful simulator error handling (Code=1 expected) - Follow Apple best practice: schedule next task immediately at execution - Ensure task completion even on expiration with guard flag - Improved error handling and structured logging Testing Coverage Expansion: - Added edge case scenarios table (7 scenarios: Background Refresh Off, Low Power Mode, Force-Quit, Timezone Change, DST, Multi-Day, Reboot) - Expanded failure injection tests (8 new negative-path scenarios) - Documented automated testing strategies (unit and integration tests) Validation Enhancements: - Added structured JSON logging schema for events - Provided log validation script (validate-ios-logs.sh) - Enhanced test run template with telemetry and state verification - Documented state integrity checks (content hash, schedule hash) - Added UI indicators and persistent test artifacts requirements Documentation Updates: - Enhanced IOS_PREFETCH_TESTING.md with comprehensive test strategies - Added Technical Correctness Requirements to IOS_TEST_APP_REQUIREMENTS.md - Expanded error handling test cases from 2 to 7 scenarios - Created ENHANCEMENTS_APPLIED.md summary document Files modified: - ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift: Enhanced with technical correctness improvements - doc/test-app-ios/IOS_PREFETCH_TESTING.md: Expanded testing coverage - doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md: Added technical requirements - doc/test-app-ios/ENHANCEMENTS_APPLIED.md: New summary document
38 KiB
iOS Prefetch Testing Guide
Purpose: How to test background prefetch for DailyNotificationPlugin on iOS (simulator + device)
Version: 1.0.1
Scope: Phase 1 Prefetch MVP
Next Target: Phase 2 (Rolling Window + TTL Telemetry)
Maintainer: Matthew Raymer
Last Updated: 2025-11-15
Status: 🎯 ACTIVE - Testing guide for Phase 1+ implementation
Note: This guide assumes the iOS test app is implemented as described in doc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md.
Glossary: See doc/test-app-ios/IOS_PREFETCH_GLOSSARY.md for terminology definitions.
Android parity: Behavior is aligned with test-apps/android-test-app where platform constraints allow. Timing and BGTask heuristics will differ from Android's exact alarms:
- Android: Exact alarms via AlarmManager / WorkManager
- iOS: Heuristic BGTaskScheduler; no hard guarantee of 5-min prefetch
Non-Goals (Phase 1)
Out of scope for Phase 1 testing:
- ❌ No testing of silent push notifications
- ❌ No testing of advanced rolling-window policies beyond "schedule one daily notification"
- ❌ No guarantees of exact N-minute timing (iOS heuristics only)
- ❌ No testing of multi-day rolling windows (deferred to Phase 2)
Table of Contents
- Goals
- Assumptions
- Simulator Test Plan (Logic-Correctness)
- Real Device Test Plan (Timing & Reliability)
- Log Checklist
- Behavior Classification
- Test Harness Reference
Goals
Verify that the iOS prefetch path actually runs:
- ✅ BGTask is registered
- ✅ BGTask is scheduled
- ✅ Fetch executes
- ✅ Data is persisted & used by the notification
Separate concerns:
- "Does the logic work?" → Test in Simulator
- "Does timing behave?" → Test on Real Device
Time & T-Lead Rules
Critical: These rules prevent bugs when tweaking prefetch timing.
Time Storage
notificationTimeis stored in UTC (not local time)- All internal calculations use UTC timestamps
- Display to users may convert to local time, but storage remains UTC
Prefetch Lead Calculation
- Prefetch lead:
prefetchTime = notificationTime - leadMinutes - Default lead: 5 minutes (300 seconds)
- Minimum lead: 1 minute (60 seconds) - iOS requires at least 1 minute in future
- Maximum lead: No hard limit, but iOS heuristics favor shorter windows
Edge Cases
Negative/Too-Small Lead Times:
- If
notificationTime - leadMinutes < now + 60 seconds:- Cap prefetch at
now + 60 seconds(minimum iOS requirement) - Log:
[DNP-FETCH] Prefetch lead too small, capping at minimum (60s) - Or disable prefetch if notification is too soon
- Cap prefetch at
DST (Daylight Saving Time) Changes:
- UTC storage prevents DST issues
- Test case: Schedule notification at 2:00 AM on DST change day
- Verify: Prefetch and notification times remain correct (UTC-based)
Timezone Changes:
- Test case: User changes timezone between schedule and fire
- Verify: Notification fires at correct UTC time (not local time)
- Log:
[DNP-SCHEDULER] Timezone changed, but UTC schedule unchanged
Test Cases
-
DST Change:
- Schedule notification for 2:00 AM on DST transition day
- Verify prefetch and notification use UTC, not local time
- Check logs show UTC timestamps
-
Timezone Change:
- Schedule notification
- Change device timezone
- Verify notification fires at correct UTC time
- Check logs confirm UTC-based scheduling
-
Too-Small Lead:
- Schedule notification 30 seconds in future
- Verify prefetch is capped at 60 seconds minimum
- Check logs show capping behavior
If You Only Have 30 Minutes
Quick validation checklist (each step links to log verification):
- Build test app (per
IOS_TEST_APP_REQUIREMENTS.md) - Run on iOS Simulator
- Schedule notification 5 minutes in future → See Log Checklist → Notification Scheduled
- Background app (Home button / Cmd+Shift+H)
- Xcode → Debug → Simulate Background Fetch → See Log Checklist → BGTask Fired
- Confirm logs show: Registration → Scheduling → BGTask handler → Fetch success → Task completed
- Wait for notification time or manually trigger → See Log Checklist → Notification Delivery
- Verify notification uses cached content (check logs for
[DNP-FETCH] Found cached content) → See Log Checklist → Notification Delivery
Assumptions
- Using BGTaskScheduler (
BGAppRefreshTask) for prefetch - App has background modes enabled:
- Background fetch
- Background processing (if using
BGProcessingTask)
- Info.plist has:
BGTaskSchedulerPermittedIdentifiersarray with task identifier:com.timesafari.dailynotification.fetch
- Plugin exposes:
scheduleDailyNotification()method that schedules both prefetch and notification
Architecture at a Glance
Flow Overview:
JS/HTML Test App → Capacitor Bridge → DailyNotificationPlugin (iOS)
↓
BGTaskScheduler (BGAppRefreshTask)
↓
HTTP API (JWT, ETag, etc.)
↓
Cache/DB (UserDefaults/CoreData)
↓
Notification Delivery
(uses cached content if valid,
falls back to live fetch)
Key Components:
- Test App: Provides UI to trigger plugin methods (see
IOS_TEST_APP_REQUIREMENTS.md) - Plugin: Handles scheduling, fetching, caching, notification delivery
- BGTaskScheduler: iOS background execution for prefetch
- Storage: Persists schedules and cached content
- Notification System: Delivers notifications using prefetched or live content
Verification Points:
- BGTask registered at app startup
- BGTask scheduled 5 minutes before notification time
- BGTask executes and fetches content
- Content persisted to cache/DB
- Notification delivery uses cached content
Simulator Test Plan (Logic-Correctness)
Objective: Confirm that when a background task fires, your prefetch code runs end-to-end.
Cross-Reference: Corresponds to IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios → Basic Functionality [SIM+DEV]
1. Harden Logging (One-Time Prep)
Add structured logs at key points:
On app startup:
[DNP-FETCH] Registering BGTaskScheduler task (id=com.timesafari.dailynotification.fetch)
When scheduling:
[DNP-FETCH] Scheduling BGAppRefreshTask (earliestBeginDate=2025-11-14T05:48:00Z)
[DNP-FETCH] Prefetch scheduled 5 minutes before notification (notificationTime=2025-11-14T05:53:00Z)
When BGTask handler fires:
[DNP-FETCH] BGTask handler invoked (task.identifier=com.timesafari.dailynotification.fetch)
Inside prefetch logic:
[DNP-FETCH] Starting fetch (notificationTime=2025-11-14T05:53:00Z, scheduleId=...)
[DNP-FETCH] Fetch success: status=200, items=1, ttl=86400
[DNP-FETCH] Fetch failed: error=..., willRetry=false
When integrating with notifications:
[DNP-FETCH] Using cached content for notification at 2025-11-14T05:53:00Z
[DNP-FETCH] No cached content, falling back to on-demand fetch
2. Start App in Simulator
- Clean build, run on iPhone 15 simulator (or similar)
- Verify console logs:
- ✅
[DNP-FETCH] Registering BGTaskScheduler task...at startup
- ✅
3. Schedule a Notification with Prefetch
- Call plugin's
scheduleDailyNotification()from JS layer - Check logs:
- ✅
[DNP-FETCH] Scheduling BGAppRefreshTask...with sensibleearliestBeginDate - ✅
[DNP-SCHEDULER] Scheduling notification for 2025-11-14T05:53:00Z - ✅ Any DB/state writes for the schedule
- ✅
Note: For a full happy-path log example, see IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios → Basic Functionality.
4. Simulate Background Fetch
With app running:
- Background the app (Home button / Cmd+Shift+H)
- In Xcode menu:
- Debug → Simulate Background Fetch or
- Debug → Simulate Background Refresh
Expected logs (in order):
[DNP-FETCH] BGTask handler invoked (task.identifier=...)
[DNP-FETCH] Starting fetch (notificationTime=..., scheduleId=...)
[DNP-FETCH] API call: GET /api/notifications (JWT present, ETag=...)
[DNP-FETCH] Fetch success: status=200, bytes=1234, ttl=86400
[DNP-FETCH] Cached content for scheduleId=...
[DNP-FETCH] Task completed (success=true)
See Sample Prefetch Response & Mapping below for expected API response structure.
Copy-Paste Commands:
# In Xcode LLDB console (simulator only):
e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"]
# Or use test app UI button: "Simulate BGTask Now"
5. Trigger or Wait for Notification
Option A: Wait for scheduled time
- Wait until scheduled time
- Look for:
[DNP-FETCH] Using cached content for notification at ...[DNP-SCHEDULER] Notification delivered (id=..., title=...)
Option B: Manual trigger
- Use test app UI to trigger notification test path
- Confirm notification shows content derived from prefetch, not fallback
6. Sample Prefetch Response & Mapping
Example API Response:
When prefetch succeeds, the API returns JSON like:
{
"id": "notif-2025-11-15",
"title": "Your daily update",
"body": "Here is today's summary…",
"ttl": 86400,
"scheduled_for": "2025-11-15T05:53:00Z"
}
Response Mapping:
id→ Used for notification identification and deduplicationtitle→ Notification title (displayed to user)body→ Notification body (displayed to user)ttl→ Cache validity check at delivery time (seconds)scheduled_for→ Cross-check withnotificationTimeto ensure alignment
Log Verification:
When you see [DNP-FETCH] Fetch success (status=200, bytes=1234, ttl=86400), the ttl=86400 should match the ttl field in the JSON response. The scheduled_for timestamp should match the notificationTime used in scheduling.
Copy-Paste Commands:
// In test app or test harness:
// View cached payload
let cachedContent = UserDefaults.standard.data(forKey: "DNP_CachedContent_\(scheduleId)")
let json = try? JSONSerialization.jsonObject(with: cachedContent ?? Data())
// Simulate time warp (testing only)
DailyNotificationBackgroundTaskTestHarness.simulateTimeWarp(minutesForward: 60)
// Force reschedule all tasks
DailyNotificationBackgroundTaskTestHarness.forceRescheduleAll()
7. Negative-Path Tests
Network failure:
- Turn off network in Simulator (Settings → Network)
- Trigger background fetch
- Expect:
[DNP-FETCH] Fetch failed: error=NetworkError, willRetry=false- Then either:
- Cached content used if available, or
- Fallback path logged:
[DNP-FETCH] No cached content, falling back to on-demand fetch
Permission denied:
- Deny notification permissions
- Attempt to schedule
- Expect:
[DNP-PLUGIN] notifications_denied for scheduleDailyNotification- Error returned:
{ error: "notifications_denied", ... }
Storage unavailable:
- Simulate DB write failure (test harness can inject this)
- Attempt to cache prefetched content
- Expect:
[DNP-STORAGE] Cache write failed: error=...- Fallback to on-demand fetch at notification time
- Log:
[DNP-FETCH] Storage unavailable, will fetch on-demand
JWT expiration:
- Simulate expired JWT token (test harness can inject this)
- Trigger background fetch
- Expect:
[DNP-FETCH] Fetch failed: error=AuthError, httpStatus=401, willRetry=false- Telemetry:
dnp_prefetch_failure_total{reason="AUTH"} - Log:
[DNP-FETCH] JWT expired, authentication failed
Timezone drift:
- Schedule notification
- Change device timezone (Settings → General → Date & Time)
- Verify:
- Notification fires at correct UTC time (not local time)
- Log:
[DNP-SCHEDULER] Timezone changed, but UTC schedule unchanged - Prefetch timing remains correct (UTC-based)
Corrupted cache:
- Manually tamper with stored cache (invalid JSON or remove entry)
- Wait for notification to fire
- Expect:
[DNP-FETCH] No cached content available, falling back to on-demand fetch- No crash on reading bad cache (error handling wraps cache read)
- Fallback fetch executes successfully
BGTask execution failure:
- Simulate internal failure in BGTask handler (test harness can inject exception)
- Verify expiration handler or completion still gets called
- Expect:
- Task marked complete even on exception (defer guard ensures completion)
- Log:
[DNP-FETCH] Task marked complete (success=false) due to error
Repeated scheduling calls:
- Call
scheduleDailyNotification()multiple times rapidly - Verify no duplicate scheduling for same time
- Expect:
- Only one BGTask is actually submitted (one active task rule)
- Logs show single scheduling, not duplicates
Permission revoked mid-run:
- Schedule and fetch (ensure success)
- Go to Settings and disable notifications before notification fires
- Expect:
- Notification won't show (iOS blocks it)
- Plugin logs absence of delivery (telemetry or log note)
- No crash if user toggles permissions
Known OS Limitations
Critical: These are iOS system limitations, not plugin bugs. If these conditions occur, it is not a plugin bug; confirm logs and document behavior.
BGTaskScheduler Simulator Limitation:
- BGTaskSchedulerErrorDomain Code=1 (notPermitted) is EXPECTED on simulator
- BGTaskScheduler doesn't work reliably on iOS Simulator - this is a known iOS limitation
- Background fetch scheduling will fail on simulator with Code=1 error
- This is NOT a plugin bug - notification scheduling still works correctly
- Prefetch won't run on simulator, but will work on real devices with Background App Refresh enabled
- Error handling logs: "Background fetch scheduling failed (expected on simulator)"
- Testing: Use Xcode → Debug → Simulate Background Fetch for simulator testing
- See also:
doc/directives/0003-iOS-Android-Parity-Directive.mdfor implementation details
iOS may NOT run BGTasks if:
- App has been force-quit by the user (iOS won't run BGTask for force-quit apps)
- Background App Refresh is disabled in Settings → [Your App]
- Device is in Low Power Mode and idle
- Device battery is critically low
- On Simulator: BGTaskScheduler generally doesn't work (Code=1 error is expected)
iOS timing heuristics:
- iOS may delay or batch tasks; prefetch might run much later than
earliestBeginDate(up to 15+ minutes) - iOS adapts BGTask frequency based on user behavior, app usage, and energy constraints
- Notification delivery can drift by ±180 seconds (this is expected, not a bug)
What to do:
- Check logs to confirm plugin logic executed correctly
- Document the OS-imposed delay in test results
- Verify fallback mechanisms work when prefetch is delayed
Real Device Test Plan (Timing & Reliability)
Objective: Confirm that prefetch happens near the intended time (e.g., 5 minutes before) in realistic conditions.
Cross-Reference: Corresponds to IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios → Background Tasks [DEV-ONLY]
1. Install Dev Build on iPhone
- Same BGTask + background modes config as simulator
- Confirm logs visible via:
- Xcode → Devices & Simulators → device → open console, or
os_logaggregator / remote logging
2. Baseline Run
- Open app
- Schedule a notification for 30–60 minutes in the future with prefetch
- Lock the phone and leave it idle, plugged in (best-case for iOS)
3. Monitor Logs
Look for:
- BGTask scheduled with appropriate
earliestBeginDate - Some time before the notification:
- BGTask handler invoked
- Prefetch fetch executes
- At notification fire:
- Notification uses prefetched data
4. Edge Case Scenarios and Environment Conditions
Test different conditions and edge cases:
| Scenario / Condition | Test Strategy | Expected Outcome & Plugin Behavior |
|---|---|---|
| Background Refresh Off | Disable Background App Refresh in iOS Settings, schedule notification, leave device idle | BGTask will not run (iOS suppresses it). Plugin logs warning (notPermitted). Notification appears using live fetch at fire time. No crash or hang. Telemetry records system failure reason. |
| Low Power Mode On | Enable Low Power Mode, schedule notification, idle device (battery vs plugged) | iOS may delay or skip BGTask. If skipped: plugin falls back gracefully. If runs later: notification uses cached data if valid. |
| App Force-Quit | Schedule notification, kill app (swipe up), keep device idle until notification time | iOS won't run BGTask (app force-quit). No prefetch occurs. Notification fires with fallback fetch. Plugin detects on next launch that prefetch didn't happen. |
| Device Timezone Change | Schedule notification for 12:00 UTC, change device timezone before it fires | Notification fires at correct UTC time (not local). Logs show UTC timestamp unchanged. Prefetch runs at UTC-5min regardless of timezone. |
| DST Transition | Schedule notification on DST change day (e.g., just after "lost hour") | Prefetch and notification align correctly in UTC. No double-firing or misses. Logs show consistent UTC times. |
| Multi-Day Scheduling (Phase 2) | Schedule two daily notifications (today and tomorrow) | Plugin handles multiple schedules. Prefetch today's content today, schedules BGTask for tomorrow. After first day, second day's prefetch still occurs. |
| Device Reboot | Schedule notification, reboot device before it fires, launch app | Schedule persists. BGTask reschedules after reboot (or app detects on launch). Startup log shows hasPendingSchedules=true. Notification still delivered. |
Additional Variations:
- Device on battery vs plugged in
- App force-quit vs just backgrounded
- Multiple days in a row: iOS will adapt its heuristics; see if prefetch becomes more or less reliable
- Low Power Mode: May delay/disable background tasks
- Background App Refresh disabled: Should fail gracefully
5. Success Criteria
In "good" conditions (plugged in, WiFi, not force-quit):
- ✅ Prefetch runs at least once before the notification in the majority of tests
- ✅ Failure modes are logged clearly:
- So you know when timing failed because of iOS, not your code
- ✅ For at least one full test cycle, logs and telemetry counts confirm that the sequence: scheduled → executed → success → used is coherent
Acceptable outcomes:
- Prefetch runs within 15 minutes of
earliestBeginDate(iOS heuristic window) - If prefetch misses, fallback to on-demand fetch works
- All failures are logged with clear reasons
Test Run Result Template
Use this template when recording test runs (especially on real devices):
Date:
Device model:
iOS version:
App build / commit:
Lead time (T-Lead - see glossary):
Scenario ID(s) tested:
Conditions: (plugged vs battery, Wi-Fi vs LTE, Low Power Mode on/off, Background App Refresh on/off)
Outcome summary:
- Prefetch scheduled at:
- BGTask executed at:
- Notification fired at:
- Cached content used: yes/no
- Actual execution time vs scheduled: (e.g., scheduled 5min before, executed 2min before = within heuristics)
Telemetry counters:
- dnp_prefetch_scheduled_total:
- dnp_prefetch_executed_total:
- dnp_prefetch_success_total:
- dnp_prefetch_used_for_notification_total:
State verification:
- Content hash match: (fetch hash vs delivery hash)
- Schedule hash match: (scheduling hash vs execution hash)
- Cache persistence: (verified after notification)
Failures observed (if any) and key log lines:
Notes / follow-ups:
Persistent Test Artifacts:
Test app can save this summary to file for later review. Access via "Export Test Results" button or UserDefaults key DNP_TestRunArtifact.
Log Checklist
When everything is wired correctly, one full cycle should produce:
1. App Launch
Expected logs:
[DNP-FETCH] Registering BGTaskScheduler task (id=com.timesafari.dailynotification.fetch)
[DNP-PLUGIN] Startup complete (hasPendingSchedules=true|false)
If you see registration but not startup:
- Check plugin initialization in AppDelegate
- Verify Capacitor plugin registration
- Check for initialization errors in logs
If you don't see registration:
- Check Info.plist has
BGTaskSchedulerPermittedIdentifiers - Verify task registered in AppDelegate before app finishes launching
- Check for registration errors in logs
2. Notification Scheduled (from JS / plugin)
Expected logs:
[DNP-FETCH] Scheduling prefetch for notification at 2025-11-14T05:53:00Z
[DNP-FETCH] BGAppRefreshTask scheduled (earliestBeginDate=2025-11-14T05:48:00Z)
[DNP-SCHEDULER] Scheduling notification for 2025-11-14T05:53:00Z
[DNP-STORAGE] Persisted schedule to DB (id=..., type=DAILY, ...)
Note: For a full happy-path log example, see IOS_TEST_APP_REQUIREMENTS.md – Testing Scenarios → Basic Functionality.
If you see scheduling but not BGTask scheduled:
- Check BGTaskScheduler submission succeeded
- Verify
earliestBeginDateis at least 1 minute in future - Check for BGTaskScheduler errors in logs
- On Simulator: BGTaskSchedulerErrorDomain Code=1 (notPermitted) is expected - see "Known OS Limitations"
If you see BGTask scheduled but not persisted:
- Check database write operations
- Verify storage layer is initialized
- Check for storage errors in logs
3. BGTask Fired (simulator or device)
Expected logs:
[DNP-FETCH] BGTask handler invoked (id=com.timesafari.dailynotification.fetch)
[DNP-FETCH] Resolved next notification needing content (time=..., scheduleId=...)
[DNP-FETCH] Starting fetch from <URL> (notificationTime=..., jwtPresent=true)
[DNP-FETCH] Fetch success (status=200, bytes=1234, ttl=86400)
[DNP-FETCH] Cached content for scheduleId=...
[DNP-FETCH] Task completed (success=true)
If you see "Registering BGTaskScheduler task" but never see "BGTask handler invoked" on device:
- Check Background App Refresh enabled in Settings → [Your App]
- Verify app not force-quit (iOS won't run BGTask for force-quit apps)
- Check Info.plist identifiers match code exactly (case-sensitive)
- Verify task was actually scheduled (check
earliestBeginDatein logs) - On Simulator: Use Xcode → Debug → Simulate Background Fetch
If you see handler invoked but not fetch starting:
- Check notification resolution logic
- Verify schedule exists in database
- Check for resolution errors in logs
If you see fetch starting but not success:
- Check network connectivity
- Verify API endpoint is accessible
- Check JWT/authentication
- Look for HTTP error codes in logs
4. Notification Delivery
Expected logs:
[DNP-SCHEDULER] Preparing content for notification at 2025-11-14T05:53:00Z
[DNP-FETCH] Found cached content (age=300s, source=prefetch)
[DNP-SCHEDULER] Notification built (title=..., body=..., id=...)
[DNP-SCHEDULER] Notification scheduled/delivered
If you see "Preparing content" but not "Found cached content":
- Check if prefetch actually ran (see BGTask logs)
- Verify cache/DB read operations
- Check TTL validation (content may have expired)
- Look for fallback to on-demand fetch
If you see "Found cached content" but not "Notification built":
- Check notification building logic
- Verify notification permissions granted
- Check for notification construction errors
5. Failure Cases
Expected logs:
[DNP-FETCH] Fetch failed (error=NetworkError, httpStatus=0, willRetry=false)
[DNP-FETCH] No cached content available, falling back to on-demand fetch
[DNP-FETCH] Task completed (success=false, reason=NETWORK)
If you see fetch failures repeatedly:
- Check network connectivity
- Verify API endpoint is correct
- Check authentication (JWT) is valid
- Look for specific error reasons in logs (NETWORK, AUTH, SYSTEM)
If you see "No cached content" but prefetch should have run:
- Check if BGTask actually executed (see BGTask logs)
- Verify cache/DB write operations succeeded
- Check for cache expiration (TTL may have passed)
- Verify fallback to on-demand fetch works
If you can walk through a log and trace all steps like this, you're in good shape.
Behavior Classification
Testable matrix of deterministic vs heuristic behavior:
| Bucket | Component / Method | Deterministic? | Test on | Notes |
|---|---|---|---|---|
| A | BGTaskScheduler.shared.register |
Yes | Sim + Dev | Registration must always log & succeed/fail deterministically |
| A | configure(), getLastNotification(), cancelAllNotifications(), getNotificationStatus(), updateSettings() |
Yes | Sim + Dev | Logic & I/O-only, no timing dependencies |
| A | checkPermissionStatus(), requestNotificationPermissions() |
Yes | Sim + Dev | Permission state reading and requests, deterministic |
| A | isChannelEnabled(channelId?), openChannelSettings(channelId?) |
Yes | Sim + Dev | Channel status and settings (iOS: app-wide, not per-channel) |
| A | getBatteryStatus(), getPowerState(), getRollingWindowStats() |
Yes | Sim + Dev | State reading, deterministic |
| A | testJWTGeneration(), testEndorserAPI() |
Yes | Sim + Dev | API call logic, deterministic |
| A | Fetch function logic (HTTP calls, DB writes, JSON parsing) | Yes | Sim + Dev | Code path is deterministic |
| A | Error handling, retry logic, fallbacks | Yes | Sim + Dev | Logic is deterministic |
| A | Log emission / structured logging | Yes | Sim + Dev | Logging is deterministic |
| A | Notification building logic (given data) | Yes | Sim + Dev | Title/body/payload construction is deterministic |
| A | TTL validation at delivery time | Yes | Sim + Dev | Validation logic is deterministic |
| B | scheduleDailyNotification() |
Logic yes, timing no | Sim + Dev | Use logs to verify scheduling but not run time |
| B | maintainRollingWindow() |
Logic yes, timing no | Sim + Dev | Logic deterministic, but when iOS allows execution is heuristic |
| B | BGTaskScheduler scheduling (earliestBeginDate) |
Logic yes, timing no | Sim + Dev | earliestBeginDate is a hint, not a guarantee |
| B | Notification trigger + prefetch relationship | Logic yes, timing no | Sim + Dev | "If cached content exists, use it" is deterministic; whether prefetch ran in time is heuristic |
| C | Time between earliestBeginDate and task execution |
No | Device | Purely heuristic, iOS controls when tasks run |
| C | Background task execution timing (BGTaskScheduler) | No | Device | iOS heuristics control execution timing |
| C | Notification delivery timing (UNUserNotificationCenter) | No | Device | iOS controls delivery timing (±180s tolerance) |
| C | Reboot recovery detection (uptime comparison) | No | Device | May vary based on system state |
Bucket Summary:
- 🟢 Bucket A (Deterministic): Test in Simulator and Device - Logic correctness
- 🟡 Bucket B (Partially Deterministic): Test flow in Simulator, timing on Device
- 🔴 Bucket C (Heuristic): Test on Real Device only - Timing and reliability
Test Harness Reference
File: ios/Plugin/DailyNotificationBackgroundTaskTestHarness.swift
Purpose: Use this harness if the plugin code is broken or being refactored; it isolates BGTask behavior from plugin wiring.
What it demonstrates:
- Task registration
- Task scheduling
- Task handler implementation
- Expiration handling
- Completion reporting
Usage:
- Reference implementation when building actual prefetch logic
- Standalone testing of BGTaskScheduler behavior
- Debugging BGTask registration/scheduling issues
- See file for detailed usage examples and testing instructions
Integration with Main Directive
This testing guide supports:
- Phase 1: Core prefetch functionality testing
- Phase 2: Advanced features testing (rolling window, TTL)
- Phase 3: TimeSafari integration testing (JWT, ETag)
- Validation Matrix: Cross-platform feature validation
Telemetry and Monitoring
Expected Counters (even if not yet implemented):
Define counters you expect the runtime to emit:
dnp_prefetch_scheduled_total- Total prefetch tasks scheduleddnp_prefetch_executed_total- Total prefetch tasks executeddnp_prefetch_success_total- Total successful prefetch executionsdnp_prefetch_failure_total{reason="NETWORK|AUTH|SYSTEM"}- Total failed prefetch executions by reasondnp_prefetch_used_for_notification_total- Total notifications using prefetched content
Telemetry Pipeline:
These counters MUST be emitted via the same pipeline as Android (e.g., structlog → rsyslog → Prometheus/Influx/Loki). If telemetry is not yet wired on iOS, mark tests that rely on counters as P2 and fall back to log inspection for Phase 1.
Telemetry JSON Schema (Phase 2 Ready):
Reserve placeholder JSON fields for telemetry events in the log schema:
{
"event": "prefetch_success",
"scheduleId": "notif-2025-11-15",
"duration_ms": 2432,
"ttl": 86400,
"timestamp": "2025-11-15T05:48:32Z",
"telemetry": {
"dnp_prefetch_scheduled_total": 1,
"dnp_prefetch_executed_total": 1,
"dnp_prefetch_success_total": 1
}
}
Even if not wired yet, this ensures Phase 2 code can emit compatible structured logs.
Telemetry Validation:
Use optional console log validation:
// In test harness or plugin
logTelemetrySnapshot(prefix: "DNP-")
This captures telemetry counters from structured logs for Phase 2 validation. Verify increment patterns (scheduled → executed → success → used).
Structured Log Output (JSON):
Log important events in structured format for automated parsing:
Success Event:
{
"event": "prefetch_success",
"id": "notif-123",
"ttl": 86400,
"fetchDurationMs": 1200,
"scheduledFor": "2025-11-15T05:53:00Z",
"timestamp": "2025-11-15T05:48:32Z"
}
Failure Event:
{
"event": "prefetch_failure",
"reason": "NETWORK",
"scheduledFor": "2025-11-15T05:53:00Z",
"timestamp": "2025-11-15T05:48:32Z"
}
Cycle Complete Summary:
{
"event": "prefetch_cycle_complete",
"id": "notif-123",
"fetched": true,
"usedCached": true,
"timestamp": "2025-11-15T05:53:00Z"
}
These structured logs can be parsed by validation scripts and analytics backends.
Success Criteria:
- For at least one full test cycle, logs and telemetry counts confirm that the sequence: scheduled → executed → success → used is coherent
- Counters increment as expected through the prefetch lifecycle
- Failure counters provide clear reason categorization
Test Campaign Sign-Off Checklist (Phase 1)
Use this checklist to verify Phase 1 prefetch testing is complete:
- Simulator: one full happy-path cycle verified (Schedule → BGTask → Fetch → Delivery)
- Device: at least one "best-case" run (plugged in, Wi-Fi, not force-quit) where prefetch runs before notification and cached content is used
- At least one failure path logged (NETWORK or PERMISSIONS) and handled gracefully
- Time & T-Lead edge-case tests run (too-small lead, timezone change) or explicitly deferred
- Telemetry counters either wired and validated, or marked P2 with log-based fallback documented
See also:
doc/directives/0003-iOS-Android-Parity-Directive.md- Main implementation directivedoc/test-app-ios/IOS_TEST_APP_REQUIREMENTS.md- Test app setup requirements
Glossary
See: doc/test-app-ios/IOS_PREFETCH_GLOSSARY.md for complete terminology definitions.
Quick Reference:
- BGTaskScheduler – iOS framework for background task scheduling (see glossary)
- T-Lead – Lead time between prefetch and notification (see glossary)
- Bucket A/B/C – Deterministic vs heuristic classification (see glossary)
- UTC – Coordinated Universal Time for timestamp storage (see glossary)
Status: 🎯 READY FOR USE
Next Steps: Use this guide when implementing and testing Phase 1+ prefetch functionality
Automated Testing Strategies
Unit Tests (Swift)
Write unit tests for plugin logic that can be tested in isolation:
Time Calculations:
- Test prefetchTime calculation:
prefetchTime = notificationTime - leadMinutes - Test edge conditions: exactly 60s lead, less than 60s lead (should cap at 60s)
- Test timezone handling: UTC storage vs local display
TTL Validation:
- Test cache validity check with simulated clock
- Test just-before-expiry vs just-after-expiry
- Test TTL expiration at notification delivery time
JSON Mapping:
- Test API response parsing with sample JSON
- Verify all fields map correctly (id, title, body, ttl, scheduled_for)
- Test error handling for malformed JSON
Permission Check Flow:
- Mock UNUserNotificationCenter returning .denied
- Verify plugin returns proper error (
notifications_denied) - Verify BGTask is not scheduled when permission denied
BGTask Scheduling Logic:
- Abstract scheduling method for dependency injection
- Inject fake BGTaskScheduler to verify correct identifier and earliestBeginDate
- Test one active task rule (cancel existing before scheduling new)
Integration Tests
Xcode UI Tests:
- Launch test app, tap "Schedule Notification"
- Background app (simulate Home button)
- Use debug simulation command to trigger BGTask
- Bring app to foreground and verify UI/logs indicate success
Log Sequence Validation:
- Run app in simulator via CLI
- Use simctl or LLDB to trigger events
- Collect logs and analyze with script
- Assert all expected log lines appear in order
Mocking and Dependency Injection:
- Network calls: Use URLProtocol to intercept HTTP and return canned responses
- Notification scheduling: Provide dummy UNUserNotificationCenter
- BGTask invocation: Call handler method directly with dummy BGTask object
- Time travel: Use injectable time source (Clock.now) for TTL/multi-day tests
Coverage of BGTask Expiration
Test expiration handler invocation:
- Set up BGTask that intentionally sleeps 40+ seconds on real device
- Monitor logs for "expirationHandler invoked" message
- Validate cancellation logic works correctly
Validation and Verification Enhancements
Structured Logging and Automated Log Analysis
Distinct Log Markers:
- Continue using prefixes:
[DNP-FETCH],[DNP-SCHEDULER],[DNP-PLUGIN],[DNP-STORAGE] - Log concise summary line at end of successful cycle:
[DNP-PLUGIN] Prefetch cycle complete (id=XYZ, fetched=true, usedCached=true)
- This gives one line to grep for overall success
Log Validation Script:
Develop validate-ios-logs.sh to parse device logs and verify sequence automatically:
#!/bin/bash
# validate-ios-logs.sh - Validates prefetch log sequence
LOG_FILE="${1:-device.log}"
# Expected sequence markers
REGISTRATION="Registering BGTaskScheduler task"
SCHEDULING="BGAppRefreshTask scheduled"
HANDLER="BGTask handler invoked"
FETCH_START="Starting fetch"
FETCH_SUCCESS="Fetch success"
TASK_COMPLETE="Task completed"
NOTIFICATION="Notification delivered"
# Check sequence
if grep -q "$REGISTRATION" "$LOG_FILE" && \
grep -q "$SCHEDULING" "$LOG_FILE" && \
grep -q "$HANDLER" "$LOG_FILE" && \
grep -q "$FETCH_START" "$LOG_FILE" && \
grep -q "$FETCH_SUCCESS" "$LOG_FILE" && \
grep -q "$TASK_COMPLETE" "$LOG_FILE" && \
grep -q "$NOTIFICATION" "$LOG_FILE"; then
echo "✅ Log sequence validated: All steps present"
exit 0
else
echo "❌ Log sequence incomplete: Missing steps"
exit 1
fi
Usage:
grep -E "\[DNP-(FETCH|SCHEDULER|PLUGIN)\]" device.log | ./scripts/validate-ios-logs.sh
Enhanced Verification Signals
Telemetry Counters:
Monitor telemetry counters for prefetch operations:
dnp_prefetch_scheduled_total- Increment when BGTask successfully scheduleddnp_prefetch_executed_total- Increment when BGTask handler runsdnp_prefetch_success_total- Increment on successful fetchdnp_prefetch_failure_total{reason="NETWORK|AUTH|SYSTEM"}- Increment on failurednp_prefetch_used_for_notification_total- Increment when notification uses cached content
State Integrity Checks:
Content Hash Verification:
- Compute MD5 hash of cached content after fetch
- Log:
[DNP-FETCH] Cached content MD5: abcdef - At notification time, verify hash matches:
[DNP-FETCH] Using cached content MD5: abcdef - Mismatch indicates content changed or wasn't used properly
Schedule Hash Verification:
- Hash scheduling data (notificationTime + lead + scheduleId)
- Print hash when scheduling and when executing
- Confirm BGTask is executing for correct schedule
Persistence Verification:
- After notification fires, verify cache state (empty if cleared, present if within TTL)
- Flag any discrepancy in logs
Persistent Test Artifacts:
Test app saves summary of each test run to file:
{
"testRun": {
"date": "2025-11-15T10:00:00Z",
"device": "iPhone 15 Pro",
"iosVersion": "17.1",
"outcome": {
"prefetchScheduledAt": "2025-11-15T05:48:00Z",
"bgTaskExecutedAt": "2025-11-15T05:50:00Z",
"notificationFiredAt": "2025-11-15T05:53:00Z",
"cachedContentUsed": true,
"errors": []
},
"telemetry": {
"scheduled": 1,
"executed": 1,
"success": 1,
"used": 1
}
}
}
Access via test app UI button "Export Test Results" or UserDefaults key DNP_TestRunArtifact.
Phase 2 Forward Plan
Planned enhancements for Phase 2:
- Implement rolling window validation
- Integrate Prometheus metrics collector
- Add automated CI pipeline for simulator validation
- Verify TTL and cache invalidation logic
- Wire telemetry counters to production pipeline
- Add automated log sequence validation
- Implement persistent schedule snapshot for post-run verification
- Add in-app log viewer/export for QA use
- Test multi-day scenarios with varying TTL values
- Validate content reuse across days when TTL allows
See also: doc/directives/0003-iOS-Android-Parity-Directive.md for Phase 2 implementation details.
Changelog (high-level)
- 2025-11-15 — Initial Phase 1 version (prefetch MVP, Android parity)