# 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: `org.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=org.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=org.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:@"org.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: `org.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=org.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=org.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:

```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)

