Files
daily-notification-plugin/doc/test-app-ios/IOS_PREFETCH_TESTING.md
Matthew Raymer b3d0d97834 docs(ios-prefetch): clarify Xcode background fetch simulation methods
Fix documentation to address Xcode behavior where 'Simulate Background
Fetch' menu item only appears when app is NOT running.

Changes:
- Add explicit note about Xcode menu item availability
- Prioritize LLDB command method when app is running (recommended)
- Document three methods: LLDB command, Xcode menu, and UI button
- Add troubleshooting section for common issues
- Update quick start section to reference LLDB method
- Explicitly reference test-apps/ios-test-app path for clarity

This resolves confusion when 'Simulate Background Fetch' disappears
from Debug menu while app is running. LLDB command method works reliably
in all scenarios.
2025-11-17 08:42:39 +00:00

39 KiB
Raw Blame History

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
  2. Assumptions
  3. Simulator Test Plan (Logic-Correctness)
  4. Real Device Test Plan (Timing & Reliability)
  5. Log Checklist
  6. Behavior Classification
  7. 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):
    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:
    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:

{
  "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:

// 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 3060 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):

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 <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 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:

{
  "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 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:

#!/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 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:

{
  "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)