test(android): add auto-reset for TEST 1 and create golden run documentation

Add automatic app state reset for TEST 1 to ensure clean starting state when
lingering alarms from TEST 0 are detected. Create PHASE1_TEST1_GOLDEN.md with
actual values from successful run.

TEST 1 Auto-Reset:
- Detect lingering plugin alarms before TEST 1 starts
- Automatically uninstall/reinstall app to clear alarms
- Verify clean state (0 alarms) before proceeding
- Gracefully skip TEST 1 if clean state cannot be achieved
- Take failure screenshots when reset fails
- Wrap all TEST 1 steps in conditional to skip on reset failure

Documentation:
- Create PHASE1_TEST1_GOLDEN.md with actual values from passing run
- Document auto-reset behavior in golden run steps
- Add cross-references between TEST 0 and TEST 1 golden docs
- Include actual timestamps, scheduleIds, and recovery metrics

This ensures TEST 1 always starts from a known clean state, making test
results reliable and reproducible. The golden doc serves as a baseline for
comparing future TEST 1 runs.
This commit is contained in:
Matthew Raymer
2025-12-04 10:22:35 +00:00
parent 1103513db3
commit ca194952e4
3 changed files with 410 additions and 6 deletions

View File

@@ -3,6 +3,10 @@
**Last Updated:** 2025-12-04
**Status:** ✅ PASS (Golden Baseline)
**Related Docs:**
- [PHASE1_TEST0_GOLDEN.md](./PHASE1_TEST0_GOLDEN.md) - Daily Rollover Verification (this document)
- [PHASE1_TEST1_GOLDEN.md](./PHASE1_TEST1_GOLDEN.md) - Force-Stop Recovery
---
## 1. Test Overview

View File

@@ -0,0 +1,327 @@
# Phase 1 — TEST 1 Golden Run (Force-Stop Recovery - Database Restoration)
**Related Docs:**
- [PHASE1_TEST0_GOLDEN.md](./PHASE1_TEST0_GOLDEN.md)
- [PHASE1_TEST1_GOLDEN.md](./PHASE1_TEST1_GOLDEN.md)
---
## 1. Test Overview
**Test Name:** TEST 1 — Force-Stop Recovery: Database Restoration
**Purpose:** Verify that after an Android **force-stop** (which clears all scheduled alarms), the plugin:
1. **Persists** the alarm schedule in its database.
2. **Detects** that alarms are missing on app launch.
3. **Rebuilds** the missing alarm(s) from the database.
4. Restores the **one-notification-per-day** contract:
- Before force-stop: 1 alarm
- After force-stop: 0 alarms
- After recovery: 1 alarm (same trigger time as before force-stop)
This golden run documents a **known-good execution** on 2025-12-04.
---
## 2. Environment & Build Info
- **Date/Time of Run:** 2025-12-04 (around 09:5109:55 UTC)
- **Command Used:**
```bash
./test-phase1.sh
```
* **Project:** `daily-notification-plugin` — `test-apps/android-test-app`
* **Gradle:**
* Build invoked via `./gradlew assembleDebug` (through the script)
* Gradle output included:
```text
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
```
* **APK Built:**
```text
./app/build/outputs/apk/debug/app-debug.apk
```
* **App ID:** `com.timesafari.dailynotification`
> **Note:** Device/emulator model & API level can be added here later if desired.
---
## 3. Step-by-Step Execution (Golden Run)
This section captures the **actual** sequence for the golden run.
1. Ran `./test-phase1.sh`.
2. Pre-flight checks:
* ✅ ADB device connected
* ✅ Emulator ready
3. Build phase:
* ✅ Debug APK built successfully.
* ✅ APK path: `./app/build/outputs/apk/debug/app-debug.apk`
4. Install phase:
* ✅ Previous app uninstall succeeded
* ✅ APK installed successfully
* ✅ App verified in package list
* ✅ Logcat cleared (twice)
5. **TEST 0** executed:
* Configured plugin in UI (if needed).
* Scheduled daily notification.
* Verified:
* Before scheduling: 0 plugin alarms.
* After scheduling: 1 plugin alarm for **today**.
* After fire/rollover: 1 plugin alarm for **tomorrow**.
* **TEST 0 PASSED.**
6. **TEST 1** started:
* Step 1: Detected 1 lingering plugin alarm (tomorrow's alarm from TEST 0).
* Auto-reset:
* Uninstalled app
* Reinstalled APK
* Cleared logcat
* Verified plugin alarms = 0
* Confirmed **clean starting state** for TEST 1.
7. Step 2: Launched app and confirmed plugin configured.
8. In UI: tapped **"Test Notification"** button (schedules a notification ~4 minutes in the future).
9. Step 3 (pre-FS verify):
* Verified **1** plugin alarm exists in AlarmManager.
* Confirmed alarm details (time, tag, type).
* Confirmed scheduling logs with `source=TEST_NOTIFICATION`.
10. Step 4: Performed **force-stop** via `adb shell am force-stop com.timesafari.dailynotification`.
11. Step 5 (post-FS verify):
* Verified plugin alarms = **0** (force-stop cleared alarms).
12. Step 6: Relaunched app (cold start).
13. Step 7 (recovery verify):
* Verified plugin alarms = **1** after recovery.
* Confirmed recovery logs under `DNP-REACTIVATION`:
* App launch recovery
* Boot recovery
* `rescheduled=1`
* Confirmed rescheduled alarm uses the **same scheduleId and triggerTime** as pre-force-stop.
14. Fire verification was **skipped** (`VERIFY_FIRE=false`).
15. TEST 1 summary:
* ✅ Before FS: 1 alarm
* ✅ After FS: 0 alarms
* ✅ After recovery: 1 alarm
* ✅ `rescheduled=1`, `errors=0`
* ✅ TEST 1 PASSED.
---
## 4. Expected Script Output (Key Excerpts)
These are the **critical excerpts** from the test harness output for a passing TEST 1.
### 4.1 Clean Start & Auto-Reset
```text
→ Step 1: Clean start - checking for lingering alarms...
Current plugin notification alarms: 1
System/other alarms: 17 (for context)
⚠️ Found 1 lingering plugin alarm(s) - these will interfere with TEST 1.
TEST 1 needs a clean state (no existing plugin alarms).
Resetting app state via uninstall + reinstall...
Uninstalling existing app...
✅ App uninstall succeeded (clean slate).
Reinstalling APK...
✅ App reinstall succeeded.
Clearing logcat buffer after reinstall...
Rechecking plugin alarms after reset...
Plugin alarms after reset: 0 (expected: 0)
✅ App state reset complete. TEST 1 starting from clean state.
```
### 4.2 Alarm Scheduled Before Force-Stop
```text
→ Step 3: Verifying alarm exists in AlarmManager (BEFORE force-stop)...
Plugin alarms: 1 (expected: 1)
System/other alarms: 19 (for context)
✅ ✅ Single plugin alarm confirmed in AlarmManager (one per day)
Alarm details:
RTC_WAKEUP #4: Alarm{161cd2b type 0 origWhen 1764842100000 whenElapsed 13009441 com.timesafari.dailynotification}
tag=*walarm*:com.timesafari.daily.NOTIFICATION
type=RTC_WAKEUP origWhen=2025-12-04 09:55:00.000 window=0 exactAllowReason=policy_permission repeatInterval=0 count=0 flags=0x3
policyWhenElapsed: requester=+3m4s281ms app_standby=-6s566ms device_idle=-- battery_saver=--
--
Alarm scheduled for: Thu Dec 4 09:55:00 AM UTC 2025 (1764842100000 ms)
Checking logs for scheduling confirmation...
12-04 09:51:49.150 6803 6867 W DNP-SCHEDULE: Cancelling existing alarm before rescheduling: requestCode=3454, scheduleId=daily_1764841909137, source=TEST_NOTIFICATION
12-04 09:51:49.151 6803 6867 I DNP-NOTIFY: Scheduling alarm: triggerTime=2025-12-04 09:55:00, delayMs=190849, requestCode=3454, scheduleId=daily_1764841909137
12-04 09:51:49.152 6803 6867 I DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, triggerTime=1764842100000, requestCode=3454, scheduleId=daily_1764841909137, source=TEST_NOTIFICATION, pendingIntentHash=267839060, showIntentHash=256236029
12-04 09:51:49.153 6803 6867 I DNP-NOTIFY: Alarm clock scheduled (setAlarmClock): triggerAt=1764842100000, requestCode=3454
```
### 4.3 Force-Stop & Alarm Clearance
```text
→ Step 4: Force-stopping app (clears all alarms)...
⚠️ Force-stop will clear ALL alarms from AlarmManager
Executing: adb shell am force-stop com.timesafari.dailynotification
Forcing stop of app process...
✅ Force stop issued
→ Step 5: Verifying alarms are MISSING from AlarmManager (AFTER force-stop)...
Plugin alarms after force-stop: 0 (expected: 0)
System/other alarms: 17 (for context)
✅ ✅ Plugin alarms cleared by force-stop (count: 0)
This confirms: Force-stop cleared alarms from AlarmManager
```
### 4.4 Recovery & Rescheduling
```text
→ Step 6: Relaunching app (triggers recovery from database)...
Clearing logcat buffer...
✅ Logs cleared
Launching app...
Starting: Intent { cmp=com.timesafari.dailynotification/.MainActivity }
✅ App launched
→ Step 7: Verifying recovery rebuilt alarms from database...
Plugin alarms after recovery: 1 (expected: 1)
System/other alarms: 21 (for context)
Checking recovery logs...
12-04 09:52:21.919 6954 7015 I DNP-REACTIVATION: Starting app launch recovery (Phase 1: cold start only)
12-04 09:52:21.926 6954 7015 I DNP-REACTIVATION: Cold start recovery: checking for missed notifications
12-04 09:52:21.937 6954 7015 I DNP-REACTIVATION: Rescheduled alarm: daily_1764841909137 for 1764842100000
12-04 09:52:21.937 6954 7015 I DNP-REACTIVATION: Rescheduled missing alarm: daily_1764841909137 at 1764842100000
12-04 09:52:21.952 6954 7015 I DNP-REACTIVATION: Cold start recovery complete: missed=0, rescheduled=1, verified=0, errors=0
12-04 09:52:21.952 6954 7015 I DNP-REACTIVATION: App launch recovery completed: missed=0, rescheduled=1, verified=0, errors=0
12-04 09:52:22.077 6954 7015 I DNP-REACTIVATION: Starting boot recovery
12-04 09:52:22.135 6954 7017 I DNP-REACTIVATION: Loaded 1 schedules from DB
12-04 09:52:22.138 6954 7017 I DNP-REACTIVATION: Rescheduled alarm: daily_1764841909137 for 1764842100000
12-04 09:52:22.143 6954 7017 I DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=1, verified=0, errors=0
```
Final summary:
```text
✅ ✅ Alarms restored in AlarmManager (count: 1)
✅ ✅ Recovery logs confirm rescheduling (rescheduled=1)
✅ TEST 1 PASSED: Recovery successfully rebuilt alarms from database!
Summary:
- Before force-stop: 1 alarm(s)
- After force-stop: 0 alarm(s) (cleared)
- After recovery: 1 alarm(s) (rebuilt)
- Rescheduled: 1 alarm(s)
- Verified: 0 alarm(s)
Skipping fire verification (VERIFY_FIRE=false, set VERIFY_FIRE=true to enable)
```
---
## 5. Expected `dumpsys alarm` Shapes
### 5.1 Before Force-Stop (Scheduled)
* **Plugin alarm count:** 1
* Example block (shape, not necessarily exact handle):
```text
RTC_WAKEUP #4: Alarm{161cd2b type 0 origWhen 1764842100000 whenElapsed 13009441 com.timesafari.dailynotification}
tag=*walarm*:com.timesafari.daily.NOTIFICATION
type=RTC_WAKEUP origWhen=2025-12-04 09:55:00.000 window=0 exactAllowReason=policy_permission repeatInterval=0 count=0 flags=0x3
policyWhenElapsed: requester=+3m4s281ms app_standby=-6s566ms device_idle=-- battery_saver=--
```
### 5.2 After Force-Stop
* **Plugin alarm count:** 0
* No `*walarm*:com.timesafari.daily.NOTIFICATION` entries should appear.
### 5.3 After Recovery
* **Plugin alarm count:** 1
* Alarm should be restored with:
* Same `scheduleId`: `daily_1764841909137`
* Same `triggerTime`: `1764842100000` → `2025-12-04 09:55:00`
---
## 6. Expected `logcat` Patterns
For a passing run, look for:
* **Scheduling (before FS):**
```text
DNP-NOTIFY: Scheduling alarm: triggerTime=2025-12-04 09:55:00, delayMs=..., requestCode=3454, scheduleId=daily_1764841909137
DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, triggerTime=1764842100000, requestCode=3454, scheduleId=daily_1764841909137, source=TEST_NOTIFICATION, ...
```
* **Recovery (after FS + relaunch):**
```text
DNP-REACTIVATION: Starting app launch recovery (Phase 1: cold start only)
DNP-REACTIVATION: Rescheduled alarm: daily_1764841909137 for 1764842100000
DNP-REACTIVATION: Cold start recovery complete: missed=0, rescheduled=1, verified=0, errors=0
DNP-REACTIVATION: App launch recovery completed: missed=0, rescheduled=1, verified=0, errors=0
DNP-REACTIVATION: Starting boot recovery
DNP-REACTIVATION: Loaded 1 schedules from DB
DNP-REACTIVATION: Rescheduled alarm: daily_1764841909137 for 1764842100000
DNP-REACTIVATION: Boot recovery complete: missed=0, rescheduled=1, verified=0, errors=0
```
Key invariants:
* `rescheduled=1`
* `errors=0`
* `scheduleId` and `triggerTime` match pre-FS values.
---
## 7. Quick Pass/Fail Checklist
A TEST 1 run is a **PASS** if all of the following are true:
* **Starting state:**
* Script auto-resets if lingering alarms exist from TEST 0.
* After reset: plugin alarm count = **0**.
* **Pre-force-stop:**
* Plugin alarm count = **1**.
* Alarm is tagged `*walarm*:com.timesafari.daily.NOTIFICATION`.
* `triggerTime` and `origWhen` are consistent (e.g. `2025-12-04 09:55:00`, `1764842100000`).
* `scheduleId` looks like `daily_<timestamp>` (here: `daily_1764841909137`).
* Logs show `source=TEST_NOTIFICATION`.
* **After force-stop:**
* Plugin alarm count = **0**.
* No NOTIFICATION alarms remain in `dumpsys`.
* **After recovery (relaunch):**
* Plugin alarm count = **1**.
* Logs show `DNP-REACTIVATION` with:
* `rescheduled=1`
* `errors=0`
* Same `scheduleId` and `triggerTime` as pre-FS.
* **Script summary:**
* States:
* Before force-stop: 1 alarm
* After force-stop: 0 alarms
* After recovery: 1 alarm
* Ends with: `✅ TEST 1 PASSED: Recovery successfully rebuilt alarms from database!`
If any of these conditions fail, the run is **NOT GOLDEN** and should not overwrite this reference.
---
## 8. Notes / Deviations
* This golden run **skips fire verification** (`VERIFY_FIRE=false`).
Future runs may enable fire verification; if that becomes standard, update this doc.
* It is acceptable for both:
* **Cold start recovery** and
* **Boot recovery**
to run on app launch, as long as:
* `rescheduled=1`
* No duplicate alarms are scheduled.
* If OS behavior changes (e.g., force-stop no longer clears alarms on a future Android version), this test's expectations may need to be revised; document any such deviations explicitly here before changing the checklist.

View File

@@ -434,16 +434,87 @@ main() {
print_step "1" "Clean start - checking for lingering alarms..."
LINGERING_COUNT=$(get_plugin_alarm_count)
SYSTEM_COUNT=$(get_system_alarm_count)
print_info "Current plugin notification alarms: ${LINGERING_COUNT}"
print_info "System/other alarms: ${SYSTEM_COUNT} (for context)"
if [ "${LINGERING_COUNT}" -gt "0" ] 2>/dev/null; then
print_warn "Found ${LINGERING_COUNT} lingering plugin alarm(s) - these may interfere with test"
print_info "System/other alarms: ${SYSTEM_COUNT} (for context)"
print_info "Consider uninstalling/reinstalling app for clean state"
wait_for_user
print_warn "Found ${LINGERING_COUNT} lingering plugin alarm(s) - these will interfere with TEST 1."
print_info "TEST 1 needs a clean state (no existing plugin alarms)."
print_info "Resetting app state via uninstall + reinstall..."
# Uninstall existing app
print_info "Uninstalling existing app..."
set +e
uninstall_output="$($ADB_BIN uninstall "$APP_ID" 2>&1)"
uninstall_status=$?
set -e
if [ $uninstall_status -eq 0 ]; then
print_success "App uninstall succeeded (clean slate)."
else
if grep -q "DELETE_FAILED_INTERNAL_ERROR" <<<"$uninstall_output"; then
print_info "No existing app to uninstall (continuing)"
else
print_warn "App uninstall reported an error: $uninstall_output (continuing anyway)"
fi
fi
# Reinstall APK
print_info "Reinstalling APK..."
if $ADB_BIN install -r "$APK_PATH" >/dev/null 2>&1; then
print_success "App reinstall succeeded."
else
print_error "App reinstall FAILED - cannot proceed with TEST 1."
print_warn "Marking TEST 1 as INCONCLUSIVE due to dirty starting state."
take_failure_screenshot "phase1_test1_force_stop" "dirty_state_reinstall_failed"
wait_for_user
# Skip to end of TEST 1 block
goto_test1_end=true
fi
if [ "${goto_test1_end:-false}" != "true" ]; then
# Verify install
if ! $ADB_BIN shell pm list packages | grep -q "$APP_ID"; then
print_error "App not found in package list after reinstall - aborting TEST 1."
take_failure_screenshot "phase1_test1_force_stop" "dirty_state_verify_failed"
wait_for_user
goto_test1_end=true
fi
fi
if [ "${goto_test1_end:-false}" != "true" ]; then
# Clear logs
print_info "Clearing logcat buffer after reinstall..."
$ADB_BIN logcat -c || true
# Re-check plugin alarms
print_info "Rechecking plugin alarms after reset..."
sleep 2 # Give system time to clear alarms
LINGERING_COUNT=$(get_plugin_alarm_count)
print_info "Plugin alarms after reset: ${LINGERING_COUNT} (expected: 0)"
if [ "${LINGERING_COUNT}" -ne "0" ] 2>/dev/null; then
print_warn "TEST 1 starting with non-zero alarm count even after reset; treating as INCONCLUSIVE."
print_info "This may indicate device-specific behavior where alarms persist across uninstall."
take_failure_screenshot "phase1_test1_force_stop" "unexpected_alarms_after_reset"
wait_for_user
goto_test1_end=true
fi
fi
if [ "${goto_test1_end:-false}" = "true" ]; then
print_warn "TEST 1 skipped due to inability to achieve clean starting state."
wait_for_user
else
print_success "App state reset complete. TEST 1 starting from clean state."
fi
else
print_success "No lingering plugin alarms found (clean state)"
print_info "System/other alarms: ${SYSTEM_COUNT} (for context)"
fi
# Skip remaining TEST 1 steps if we couldn't achieve clean state
if [ "${goto_test1_end:-false}" != "true" ]; then
# ============================================
# Step 2: Schedule a known future alarm
# ============================================
@@ -664,8 +735,10 @@ main() {
print_info "Skipping fire verification (VERIFY_FIRE=false, set VERIFY_FIRE=true to enable)"
fi
fi # End of "if goto_test1_end != true" block (wraps all TEST 1 steps after clean state check)
wait_for_user
fi
fi # End of "if should_run_test 1" block
# ============================================
# TEST 2: Schedule Update (One-Per-Day Semantics)