# 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`. The test app is located at `test-apps/ios-test-app/`. **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 1. [Goals](#goals) 2. [Assumptions](#assumptions) 3. [Simulator Test Plan (Logic-Correctness)](#simulator-test-plan) 4. [Real Device Test Plan (Timing & Reliability)](#real-device-test-plan) 5. [Log Checklist](#log-checklist) 6. [Behavior Classification](#behavior-classification) 7. [Test Harness Reference](#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 - **`notificationTime` is 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 **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 1. **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 2. **Timezone Change:** - Schedule notification - Change device timezone - Verify notification fires at correct UTC time - Check logs confirm UTC-based scheduling 3. **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): 1. Build test app (per `IOS_TEST_APP_REQUIREMENTS.md`): ```bash cd test-apps/ios-test-app # Or from repo root: ./scripts/build-ios-test-app.sh --simulator ``` 2. Run on iOS Simulator 3. Schedule notification 5 minutes in future β†’ **See Log Checklist β†’ Notification Scheduled** 4. Background app (Home button / Cmd+Shift+H) 5. **Trigger BGTask** (see Step 4 below for methods): - **Recommended:** Use LLDB command in Xcode console (app must be running) - **Alternative:** Stop app, then use Xcode β†’ Debug β†’ Simulate Background Fetch - **See Log Checklist β†’ BGTask Fired** 6. Confirm logs show: Registration β†’ Scheduling β†’ BGTask handler β†’ Fetch success β†’ Task completed 7. Wait for notification time or manually trigger β†’ **See Log Checklist β†’ Notification Delivery** 8. 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: - `BGTaskSchedulerPermittedIdentifiers` array 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 1. Clean build, run on iPhone 15 simulator (or similar) 2. Verify console logs: - βœ… `[DNP-FETCH] Registering BGTaskScheduler task...` at startup ### 3. Schedule a Notification with Prefetch 1. Call plugin's `scheduleDailyNotification()` from JS layer 2. Check logs: - βœ… `[DNP-FETCH] Scheduling BGAppRefreshTask...` with sensible `earliestBeginDate` - βœ… `[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 **Important:** Xcode's "Simulate Background Fetch" menu item only appears when the app is **NOT running**. When the app is running, use the LLDB command method below. **Method 1: LLDB Command (Recommended when app is running)** 1. **Background the app** (Home button / Cmd+Shift+H in simulator) 2. **Open Xcode Debug Console** (View β†’ Debug Area β†’ Activate Console, or press Cmd+Shift+Y) 3. **In LLDB console, paste and execute:** ```bash e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.timesafari.dailynotification.fetch"] ``` 4. Press Enter **Method 2: Xcode Menu (Only when app is NOT running)** 1. **Stop the app** (Cmd+.) 2. In Xcode menu: - **Debug β†’ Simulate Background Fetch** or - **Debug β†’ Simulate Background Refresh** 3. This will launch the app and trigger the background fetch **Method 3: Test App UI Button (If implemented)** - Use test app UI button: "Simulate BGTask Now" (if available in test app) **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.** **Troubleshooting:** - If LLDB command doesn't work, ensure the app is backgrounded first - If you see "Simulate Background Fetch" in menu when app is running, it may be a different Xcode version - try the LLDB method anyway - Verify BGTask identifier matches exactly: `com.timesafari.dailynotification.fetch` ### 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: ```json { "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 deduplication - `title` β†’ Notification title (displayed to user) - `body` β†’ Notification body (displayed to user) - `ttl` β†’ Cache validity check at delivery time (seconds) - `scheduled_for` β†’ Cross-check with `notificationTime` to 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:** ```swift // 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:** 1. Turn off network in Simulator (Settings β†’ Network) 2. Trigger background fetch 3. 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:** 1. Deny notification permissions 2. Attempt to schedule 3. Expect: - `[DNP-PLUGIN] notifications_denied for scheduleDailyNotification` - Error returned: `{ error: "notifications_denied", ... }` **Storage unavailable:** 1. Simulate DB write failure (test harness can inject this) 2. Attempt to cache prefetched content 3. 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:** 1. Simulate expired JWT token (test harness can inject this) 2. Trigger background fetch 3. 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:** 1. Schedule notification 2. Change device timezone (Settings β†’ General β†’ Date & Time) 3. 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:** 1. Manually tamper with stored cache (invalid JSON or remove entry) 2. Wait for notification to fire 3. 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:** 1. Simulate internal failure in BGTask handler (test harness can inject exception) 2. Verify expiration handler or completion still gets called 3. 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:** 1. Call `scheduleDailyNotification()` multiple times rapidly 2. Verify no duplicate scheduling for same time 3. Expect: - Only one BGTask is actually submitted (one active task rule) - Logs show single scheduling, not duplicates **Permission revoked mid-run:** 1. Schedule and fetch (ensure success) 2. Go to Settings and disable notifications before notification fires 3. 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.md` for 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_log` aggregator / remote logging ### 2. Baseline Run 1. Open app 2. Schedule a notification for **30–60 minutes in the future** with prefetch 3. 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): ```text 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 `earliestBeginDate` is 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 (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 `earliestBeginDate` in 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 scheduled - `dnp_prefetch_executed_total` - Total prefetch tasks executed - `dnp_prefetch_success_total` - Total successful prefetch executions - `dnp_prefetch_failure_total{reason="NETWORK|AUTH|SYSTEM"}` - Total failed prefetch executions by reason - `dnp_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: ```json { "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: ```swift // 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:** ```json { "event": "prefetch_success", "id": "notif-123", "ttl": 86400, "fetchDurationMs": 1200, "scheduledFor": "2025-11-15T05:53:00Z", "timestamp": "2025-11-15T05:48:32Z" } ``` **Failure Event:** ```json { "event": "prefetch_failure", "reason": "NETWORK", "scheduledFor": "2025-11-15T05:53:00Z", "timestamp": "2025-11-15T05:48:32Z" } ``` **Cycle Complete Summary:** ```json { "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 directive - `doc/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: ```bash #!/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:** ```bash 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 scheduled - `dnp_prefetch_executed_total` - Increment when BGTask handler runs - `dnp_prefetch_success_total` - Increment on successful fetch - `dnp_prefetch_failure_total{reason="NETWORK|AUTH|SYSTEM"}` - Increment on failure - `dnp_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: ```json { "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)