test(android): fix alarm counting logic and add screenshot capture

Fix alarm counting to correctly parse dumpsys output where app ID and
action appear on different lines. Add screenshot capture for test
diagnostics and create golden run documentation.

Test Harness Improvements:
- Fix get_plugin_alarm_count() to track app ID and action separately
  across alarm block lines (fixes false 0-count bug)
- Add show_plugin_alarms_compact() to display complete alarm blocks
- Add wait_for_stable_plugin_alarm_count() polling helper to reduce
  race condition false negatives
- Add take_screenshot() and take_failure_screenshot() helpers for
  automatic test state capture
- Integrate screenshots into TEST 0 at key checkpoints
- Update TEST 0 messaging to handle race conditions gracefully
- Add screenshots/ to .gitignore

Documentation:
- Create PHASE1_TEST0_GOLDEN.md with actual values from successful run
- Document expected script output, UI state, dumpsys shape, and logcat
  patterns
- Include pass/fail checklist for future test runs

This fixes the issue where alarm counting always returned 0 because the
AWK logic required app ID and action on the same line, but dumpsys
output has them on separate lines (header line has app ID, tag line
has action).
This commit is contained in:
Matthew Raymer
2025-12-04 09:28:28 +00:00
parent fc2f64bae3
commit 1103513db3
4 changed files with 476 additions and 28 deletions

View File

@@ -23,6 +23,10 @@
: "${REACTIVATION_TAG:=DNP-REACTIVATION}"
: "${SCENARIO_KEY:=Detected scenario: }"
# Screenshot configuration
: "${SCREENSHOT_ROOT:=screenshots}"
: "${ENABLE_SCREENSHOTS:=1}"
# Derived config (for backward compatibility with Phase 1)
PACKAGE="${APP_ID}"
ACTIVITY="${APP_ID}/.MainActivity"
@@ -262,46 +266,64 @@ get_plugin_alarm_count() {
# Returns count of ONLY the plugin's NOTIFICATION alarms (not prefetch - that uses WorkManager)
# Expected: 1 notification alarm per daily schedule
#
# This function counts ALARM_CLOCK wake alarms (RTC_WAKEUP) tagged as:
# tag=*walarm*:com.timesafari.daily.NOTIFICATION
#
# Uses deduplicating parser to avoid double-counting the same alarm that appears in both:
# - Main alarm list
# - "Next wake from idle" section (ignored - only counts RTC_WAKEUP blocks)
# - Alarm Stats section (ignored - only counts actual alarm blocks)
#
# Tracks unique Alarm handles to ensure each alarm is counted only once
local count app_id
# Tracks unique Alarm handles to ensure each alarm is counted only once.
# Checks for app package AND action string anywhere in the block (they appear on different lines).
local count app_id action
app_id="$APP_ID"
count="$($ADB_BIN shell dumpsys alarm 2>/dev/null | awk -v app="$app_id" '
action="com.timesafari.daily.NOTIFICATION"
count="$($ADB_BIN shell dumpsys alarm 2>/dev/null | awk -v app="$app_id" -v action="$action" '
BEGIN {
in_block = 0
alarmId = ""
isPluginNotification = 0
hasAppLine = 0
hasActionLine = 0
}
# Start of a new RTC_WAKEUP alarm block
/^[[:space:]]*RTC_WAKEUP/ {
# Flush previous block if it was a plugin notification
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
seen[alarmId] = 1
}
in_block = 1
alarmId = ""
isPluginNotification = 0
if (match($0, /Alarm\{/)) {
rest = substr($0, RSTART + RLENGTH)
if (match(rest, /^[0-9a-f]+/)) {
alarmId = substr(rest, RSTART, RLENGTH)
}
hasAppLine = 0
hasActionLine = 0
# Extract alarmId from "Alarm{11245c ..."
if (match($0, /Alarm\{[0-9a-f]+/)) {
# match is like "Alarm{11245c", extract just the hex part
alarmId = substr($0, RSTART + 6, RLENGTH - 6)
}
}
# Blank line = end of block
/^[[:space:]]*$/ {
if (in_block == 1 && isPluginNotification == 1 && alarmId != "") {
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
seen[alarmId] = 1
}
in_block = 0
alarmId = ""
isPluginNotification = 0
hasAppLine = 0
hasActionLine = 0
}
# Lines inside an alarm block
in_block == 1 {
if ($0 ~ app && $0 ~ /com\.timesafari\.daily\.NOTIFICATION/) {
isPluginNotification = 1
if ($0 ~ app) {
hasAppLine = 1
}
if ($0 ~ action) {
hasActionLine = 1
}
}
END {
if (in_block == 1 && isPluginNotification == 1 && alarmId != "") {
# Flush final block if it was a plugin notification
if (in_block == 1 && hasAppLine == 1 && hasActionLine == 1 && alarmId != "") {
seen[alarmId] = 1
}
count = 0
@@ -341,6 +363,154 @@ count_alarms() {
get_plugin_alarm_count
}
show_plugin_alarms_compact() {
# Prints only the plugin's alarm block(s) for debugging
# Shows complete RTC_WAKEUP alarm blocks that contain the app ID
# This makes it visually obvious why the AWK matcher should pick up alarms
# (app ID on header line, action on tag line within the same block)
$ADB_BIN shell dumpsys alarm 2>/dev/null \
| awk -v app="$APP_ID" '
BEGIN {
in_block = 0
block = ""
found_app = 0
}
/^[[:space:]]*RTC_WAKEUP/ {
# Print previous block if it contained app ID
if (in_block && found_app) {
print block ORS
}
# Start new block
in_block = 1
block = $0 ORS
found_app = ($0 ~ app) ? 1 : 0
next
}
/^[[:space:]]*$/ {
# End of block
if (in_block && found_app) {
print block ORS
}
in_block = 0
block = ""
found_app = 0
next
}
{
if (in_block) {
block = block $0 ORS
if ($0 ~ app) {
found_app = 1
}
}
}
END {
if (in_block && found_app) {
print block
}
}
' \
| sed -n '1,80p' || true
}
wait_for_stable_plugin_alarm_count() {
# Polls for plugin alarm count to stabilize (reduces race condition false negatives)
# Usage: wait_for_stable_plugin_alarm_count [attempts] [delay_seconds]
# Default: 5 attempts, 2 second delay (total ~10 seconds)
# Returns: alarm count (0 if none found after all attempts)
local attempts=${1:-5}
local delay=${2:-2}
local count=0
local i
for i in $(seq 1 "$attempts"); do
count="$(get_plugin_alarm_count)"
if [ "$count" -ge 1 ] 2>/dev/null; then
echo "$count"
return 0
fi
if [ "$i" -lt "$attempts" ]; then
sleep "$delay"
fi
done
echo "$count"
}
# --- Screenshot Helpers ---
take_screenshot() {
# Captures a device screenshot and saves it with test name, step, and timestamp
# Usage: take_screenshot "test_name" "step_name"
# Example: take_screenshot "phase1_test0_daily_rollover" "before_scheduling"
local test_name="$1"
local step_name="$2"
# Do nothing if screenshots are disabled
if [ "$ENABLE_SCREENSHOTS" != "1" ]; then
return 0
fi
if [ -z "$ADB_BIN" ]; then
echo "⚠️ ADB_BIN is not set; cannot take screenshot." >&2
return 0
fi
# Timestamp for uniqueness
local ts
ts="$(date '+%Y%m%d-%H%M%S' 2>/dev/null)" || ts="unknown"
# Directory: screenshots/<test_name>/
# Use absolute path relative to script directory if SCREENSHOT_ROOT is relative
local dir
if [ -n "$SCRIPT_DIR" ] && [ -d "$SCRIPT_DIR" ]; then
dir="${SCRIPT_DIR}/${SCREENSHOT_ROOT}/${test_name}"
elif [ -n "${BASH_SOURCE[0]}" ]; then
# Fallback: derive from this script's location
local lib_dir
lib_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd 2>/dev/null)" || lib_dir=""
if [ -n "$lib_dir" ]; then
dir="${lib_dir}/${SCREENSHOT_ROOT}/${test_name}"
else
dir="${SCREENSHOT_ROOT}/${test_name}"
fi
else
dir="${SCREENSHOT_ROOT}/${test_name}"
fi
mkdir -p "$dir" 2>/dev/null || {
echo "⚠️ Failed to create screenshot directory: $dir" >&2
return 0
}
# File name: <test>_<step>_<timestamp>.png, with spaces converted to dashes
local safe_step="${step_name// /-}"
local file="${dir}/${test_name}_${safe_step}_${ts}.png"
echo "📸 Capturing screenshot: ${file}" >&2
# Use exec-out to avoid newline mangling
if ! "$ADB_BIN" exec-out screencap -p > "$file" 2>/dev/null; then
echo "⚠️ Failed to capture screenshot via adb." >&2
# Clean up empty file if created
[ -s "$file" ] || rm -f "$file" 2>/dev/null || true
return 0
fi
# Verify file was created and has content
if [ ! -s "$file" ]; then
echo "⚠️ Screenshot file is empty or missing: $file" >&2
rm -f "$file" 2>/dev/null || true
return 0
fi
}
take_failure_screenshot() {
# Convenience wrapper for failure cases
# Usage: take_failure_screenshot "test_name" "reason"
# Example: take_failure_screenshot "phase1_test0_daily_rollover" "no_alarm_after_rollover"
local test_name="$1"
local reason="$2"
take_screenshot "$test_name" "FAIL_${reason}"
}
force_stop_app() {
info "Forcing stop of app process..."
$ADB_BIN shell am force-stop "$APP_ID" || true

View File

@@ -0,0 +1,230 @@
# Phase 1 — TEST 0 Golden Run (Daily Rollover Verification)
**Last Updated:** 2025-12-04
**Status:** ✅ PASS (Golden Baseline)
---
## 1. Test Overview
This document captures a **golden baseline** for **Phase 1 TEST 0: Daily Rollover Verification**.
**Purpose:** Verify that after a notification fires, the plugin:
- Computes **next day's time** (T + 24h)
- Schedules **exactly one** `AlarmManager` notification alarm for tomorrow
- Does **not** create duplicates
- Leaves prefetch in **WorkManager** (not visible in `dumpsys alarm`)
> **Golden Rule:** If a future run looks like this doc, TEST 0 should be considered a PASS.
---
## 2. Environment & Build Info
### Emulator / Device
- **API Level:** 35
- **Android Version:** 15
### Build
- **Gradle Version:** 8.13
- **Build Warnings:**
- `WARNING: Using flatDir should be avoided because it doesn't support any meta-data formats.`
- `Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.`
### Command Used
```bash
./test-phase1.sh
```
### Environment Variables
- `ENABLE_SCREENSHOTS=1` (screenshots enabled)
- `ADB_BIN=adb` (default)
---
## 3. Step-by-Step Execution (Golden Run)
1. Ran `./test-phase1.sh`.
2. Confirmed pre-flight checks (ADB device + emulator ready).
3. Allowed script to rebuild and reinstall the app.
4. Confirmed plugin status in the UI:
- ⚙️ Plugin Settings: ✅ Configured
- 🔌 Native Fetcher: ✅ Configured
- 🔔 Notifications: ✅ Granted
- ⏰ Exact Alarms: ✅ Granted
- 📢 Channel: ✅ Enabled (High)
5. From the UI, scheduled a **daily notification** for ~12 minutes in the future (scheduled for `09:23:00` on 2025-12-04).
6. Waited for the notification banner to fire.
7. Pressed Enter to continue when prompted.
8. Let the script perform the post-rollover alarm check.
---
## 4. Expected Script Output (Key Excerpts)
### 4.1. Pre-Schedule Check
```text
✅ Found 1 notification alarm (expected: 1) preliminary check passed.
This is preliminary check only; final verdict after rollover.
```
### 4.2. Notification Alarm Details (After Scheduling)
```text
Notification alarm details:
tag=*walarm*:com.timesafari.daily.NOTIFICATION
type=RTC_WAKEUP origWhen=2025-12-04 09:23:00.000 window=0 exactAllowReason=policy_permission repeatInterval=0 count=0 flags=0x3
policyWhenElapsed: requester=+3m34s315ms app_standby=-10s456ms device_idle=-- battery_saver=--
```
### 4.3. Post-Rollover Check
```text
Polling for stable alarm count (allowing up to ~10 seconds for Android to settle)...
Notification alarms after rollover: 1 (expected: 1)
System/other alarms: <N> (for context)
Note: Prefetch is scheduled via WorkManager (not AlarmManager), so it won't appear in alarm count
```
### 4.4. Final Verdict
```text
✅ TEST 0 PASSED: Daily rollover created exactly one NOTIFICATION alarm for tomorrow.
Expected state after rollover:
✅ 1 notification alarm (AlarmManager) for tomorrow
✅ 1 prefetch job (WorkManager) for 2 minutes before tomorrow's notification
```
**Note:** The `origWhen` for tomorrow will be the next day at the same time (e.g., `2025-12-05 09:23:00.000` if scheduled for `2025-12-04 09:23:00.000`).
---
## 5. Expected UI State (Screenshots)
### 5.1 Screenshot Files (Golden Run)
- `screenshots/phase1_test0_daily_rollover/phase1_test0_daily_rollover_before_scheduling_20251204-091910.png`
- **Status:** Active Schedules: **No**; Next Notification: **None scheduled**.
- `screenshots/phase1_test0_daily_rollover/phase1_test0_daily_rollover_after_scheduling_20251204-091925.png`
- **Status:** Active Schedules: **Yes**; Next Notification: **today at 09:23:00 AM**; Pending: **1**.
- `screenshots/phase1_test0_daily_rollover/phase1_test0_daily_rollover_after_rollover_check_20251204-092307.png`
- **Status:** Active Schedules: **Yes**; Next Notification: **tomorrow at 09:23:00 AM** (24 hours later); Pending: **1**.
---
## 6. Expected `dumpsys alarm` Shape
### 6.1. Representative Snippet
```text
RTC_WAKEUP #<N>: Alarm{<handle> type 0 origWhen <timestamp> whenElapsed ... com.timesafari.dailynotification}
tag=*walarm*:com.timesafari.daily.NOTIFICATION
type=RTC_WAKEUP origWhen=2025-12-05 09:23:00.000 ...
...
Next wake from idle: Alarm{<handle> type 0 origWhen <timestamp> ... com.timesafari.dailynotification}
tag=*walarm*:com.timesafari.daily.NOTIFICATION
```
### 6.2. Key Observations
- There should be **exactly one unique alarm handle** for the plugin (the handle will differ between runs).
- It can appear both in the main list and in **"Next wake from idle"**, but counted as **one** alarm (deduplication by alarm handle).
- `tag` must be `*walarm*:com.timesafari.daily.NOTIFICATION`.
- `type` must be `RTC_WAKEUP`.
- `origWhen` should be **tomorrow** at the same time-of-day as the scheduled notification (e.g., `2025-12-05 09:23:00.000` if scheduled for `2025-12-04 09:23:00.000`).
---
## 7. Expected `logcat` Patterns
### 7.1. Scheduling Test Notification
```text
DNP-SCHEDULE: Scheduling next daily alarm: id=daily_..., nextRun=2025-12-04 09:23:00, source=TEST_NOTIFICATION
DNP-NOTIFY: Stored notification content in database: id=daily_...
DNP-NOTIFY: Scheduling alarm: triggerTime=2025-12-04 09:23:00, ...
DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, ...
```
### 7.2. Rollover on Fire
```text
DNP-SCHEDULE: Scheduling next daily alarm: id=daily_rollover_..., nextRun=2025-12-05 09:23:00, source=ROLLOVER_ON_FIRE
DNP-NOTIFY: Stored notification content in database: id=notify_...
DNP-NOTIFY: Scheduling alarm: triggerTime=2025-12-05 09:23:00, ...
DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, ...
```
### 7.3. Critical Requirements
**Both sequences must be present** for a true PASS:
- `source=TEST_NOTIFICATION` sequence when scheduling the initial test notification
- `source=ROLLOVER_ON_FIRE` sequence when the notification fires and schedules tomorrow's alarm
- Times must match: initial schedule time → tomorrow's time (T + 24h)
- Example: `2025-12-04 09:23:00``2025-12-05 09:23:00`
---
## 8. Quick Pass/Fail Checklist
A run of TEST 0 is a **PASS** if all of the following are true:
### Script Output
- [ ] Shows "Found 1 notification alarm (expected: 1) preliminary check passed."
- [ ] Shows "Notification alarms after rollover: 1 (expected: 1)".
- [ ] Ends with "✅ TEST 0 PASSED: Daily rollover created exactly one NOTIFICATION alarm for tomorrow."
### UI State
- [ ] **Before scheduling:** Active Schedules: No; Next Notification: None scheduled.
- [ ] **After scheduling:** Active Schedules: Yes; Next Notification: *today* at the chosen time.
- [ ] **After rollover:** Active Schedules: Yes; Next Notification: *tomorrow* at the same time.
### `dumpsys alarm`
- [ ] Exactly one `RTC_WAKEUP` alarm with `tag=*walarm*:com.timesafari.daily.NOTIFICATION` for **tomorrow**.
- [ ] Same alarm handle may appear under "Next wake from idle", but no second distinct handle.
- [ ] `origWhen` timestamp is exactly 24 hours after the initial scheduled time.
### `logcat`
- [ ] Shows both `source=TEST_NOTIFICATION` and `source=ROLLOVER_ON_FIRE` sequences with matching times.
- [ ] No duplicate `DNP-SCHEDULE` entries for the same `nextRun` time.
- [ ] No errors or warnings related to alarm scheduling.
---
## 9. Notes / Deviations
### Failure Conditions
- If there is exactly **0** alarms after rollover, treat as **INCONCLUSIVE** and investigate:
- Check `logcat` for `ROLLOVER_ON_FIRE` sequence
- Verify `dumpsys alarm` manually
- Check for scheduling errors in logs
- If there are **>1** alarms after rollover, treat as **FAIL** (duplicate alarm bug):
- Check for multiple `DNP-SCHEDULE` entries with same `nextRun`
- Verify idempotence checks are working
- Check for race conditions between rollover and recovery paths
### Time-of-Day Variations
- Time-of-day may differ in future golden runs; **structure and relationships must remain the same**.
- The key is: initial time → tomorrow's time (T + 24h), not the specific hour/minute.
### Screenshot Timestamps
- Screenshot filenames include timestamps (`YYYYMMDD-HHMMSS`), so exact filenames will differ between runs.
- Focus on the **content** of screenshots (UI state) rather than exact filenames.
### Alarm Handle Variations
- The alarm handle (e.g., `Alarm{1f00a1b}`) will differ between runs; this is expected.
- The important thing is that there is **exactly one unique handle** per scheduled alarm.
---
## 10. Updating This Document
When updating this golden run document:
1. Update timestamps and IDs with actual values from your successful run
2. Replace placeholder values (marked with "Update...") with real data
3. Update screenshot filenames with actual timestamps
4. Add any environment-specific notes that might affect future runs
5. Document any deviations or edge cases encountered
**Last Golden Run Date:** 2025-12-04 (09:23:00 scheduled time)

View File

@@ -226,6 +226,9 @@ main() {
launch_app
ensure_plugin_configured
# Capture pre-schedule UI state
take_screenshot "phase1_test0_daily_rollover" "before_scheduling"
INITIAL_COUNT=$(get_plugin_alarm_count)
SYSTEM_COUNT=$(get_system_alarm_count)
print_info "Current notification alarms: ${INITIAL_COUNT} (expected before scheduling: 0)"
@@ -243,22 +246,30 @@ main() {
- 1 notification alarm (AlarmManager) for the specified time
- 1 prefetch job (WorkManager) for 2 minutes before that time"
sleep 3 # Give alarm time to be registered in AlarmManager
# Capture post-schedule UI state
take_screenshot "phase1_test0_daily_rollover" "after_scheduling"
POST_SCHEDULE_COUNT=$(get_plugin_alarm_count)
# Check alarm count after scheduling
# Note: 0 alarms is likely a race condition (alarm may not be visible yet in dumpsys)
# Note: This is a PRELIMINARY sanity check only - final verdict comes after rollover
# 0 alarms is likely a race condition (alarm may not be visible yet in dumpsys)
# Only treat >1 alarms as a real failure (duplicates)
if [ "${POST_SCHEDULE_COUNT}" -eq "0" ] 2>/dev/null; then
print_warn "⚠️ Found 0 plugin alarms right after scheduling."
print_info " This is likely a race condition treating as inconclusive, not a failure."
print_info " The alarm may not be visible in dumpsys yet. We'll rely on the rollover check."
print_info " Pre-schedule check not reliable; will rely on rollover assertions."
print_info " The alarm may not be visible in dumpsys yet. Continuing to verify after rollover."
elif [ "${POST_SCHEDULE_COUNT}" -eq "1" ] 2>/dev/null; then
print_success "✅ Found 1 notification alarm (expected: 1) immediate post-schedule check passed."
print_success "✅ Found 1 notification alarm (expected: 1) preliminary check passed."
print_info " This is preliminary check only; final verdict after rollover."
else
# count > 1 - this is a real duplicate bug
print_warn "⚠️ ⚠️ Found ${POST_SCHEDULE_COUNT} notification alarms (expected: 1) DUPLICATES DETECTED right after scheduling!"
print_warn "This indicates duplicate NOTIFICATION alarms were created (BUG DETECTED)"
print_info "For debugging, run:"
print_info "Showing alarm blocks for debugging:"
show_plugin_alarms_compact
print_info "For more details, run:"
print_info " adb shell dumpsys alarm | grep -A 5 'com.timesafari.dailynotification' | sed -n '1,80p'"
fi
INITIAL_COUNT="${POST_SCHEDULE_COUNT}"
@@ -316,12 +327,19 @@ main() {
echo " - Prefetch job scheduled in WorkManager"
echo ""
echo "Expected log patterns:"
echo " DNP-PLUGIN: Calculated next run time: cron=<time>"
echo " DNP-SCHEDULE: Scheduling next daily alarm: ... source=ROLLOVER_ON_FIRE"
echo " DNP-NOTIFY: Scheduling alarm: triggerTime=<tomorrow's time>"
echo " DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, ..."
echo ""
echo "When scheduling the test notification, expect:"
echo " DNP-SCHEDULE: ... source=TEST_NOTIFICATION"
echo ""
echo "Alarm shape in dumpsys:"
echo " RTC_WAKEUP, ALARM_CLOCK, tag=*walarm*:com.timesafari.daily.NOTIFICATION"
echo ""
echo "After notification fires, run:"
echo " adb shell dumpsys alarm | grep -A 3 'com.timesafari.dailynotification'"
echo " adb logcat -d | grep -E 'DNP-PLUGIN|DNP-NOTIFY' | tail -20"
echo " adb logcat -d | grep -E 'DNP-SCHEDULE|DNP-NOTIFY' | tail -20"
echo ""
wait_for_ui_action "After the notification fires (or you advance the clock),
@@ -333,12 +351,16 @@ main() {
Press Enter when you've verified this (or to skip this test)."
POST_ROLLOVER_COUNT=$(get_plugin_alarm_count)
print_info "Polling for stable alarm count (allowing up to ~10 seconds for Android to settle)..."
POST_ROLLOVER_COUNT=$(wait_for_stable_plugin_alarm_count 5 2)
SYSTEM_FINAL=$(get_system_alarm_count)
print_info "Notification alarms after rollover: ${POST_ROLLOVER_COUNT} (expected: 1)"
print_info "System/other alarms: ${SYSTEM_FINAL} (for context)"
print_info "Note: Prefetch is scheduled via WorkManager (not AlarmManager), so it won't appear in alarm count"
# Capture post-rollover UI state
take_screenshot "phase1_test0_daily_rollover" "after_rollover_check"
# After rollover, the state should be stable - this is the real assertion point
if [ "${POST_ROLLOVER_COUNT}" -eq "1" ] 2>/dev/null; then
print_success "✅ TEST 0 PASSED: Daily rollover created exactly one NOTIFICATION alarm for tomorrow."
@@ -348,15 +370,40 @@ main() {
elif [ "${POST_ROLLOVER_COUNT}" -gt "1" ] 2>/dev/null; then
print_warn "⚠️ ⚠️ TEST 0 FAILED: Daily rollover created ${POST_ROLLOVER_COUNT} NOTIFICATION alarms (duplicates)."
print_warn "This indicates duplicate NOTIFICATION alarms were created (BUG DETECTED)"
print_info "For debugging, run:"
print_info " adb shell dumpsys alarm | grep -A 5 'com.timesafari.dailynotification' | sed -n '1,80p'"
take_failure_screenshot "phase1_test0_daily_rollover" "duplicate_alarms"
echo ""
print_info "Showing alarm blocks for debugging:"
show_plugin_alarms_compact
echo ""
print_info "Check scheduling logs for multiple DNP-SCHEDULE entries:"
print_info " adb logcat -d | grep 'DNP-SCHEDULE\|DNP-NOTIFY' | tail -20"
echo ""
print_info "Possible causes:"
echo " - Multiple scheduling paths racing (rollover + recovery)"
echo " - Old alarm wasn't canceled before scheduling new one"
echo " - Different scheduleIds used for same trigger time"
echo " - Idempotence check not working correctly"
else
# count is 0 or invalid - rollover may have failed
print_warn "⚠️ TEST 0 INCONCLUSIVE: No NOTIFICATION alarm found after rollover check logs/dumpsys manually."
print_warn "⚠️ TEST 0 INCONCLUSIVE: No NOTIFICATION alarm found after rollover (expected 1, got 0)"
print_warn "This indicates the rollover did not schedule tomorrow's alarm correctly"
take_failure_screenshot "phase1_test0_daily_rollover" "no_alarm_after_rollover"
echo ""
print_info "Showing alarm blocks for debugging:"
show_plugin_alarms_compact
echo ""
print_info "Expected log pattern after fire:"
echo " DNP-SCHEDULE: Scheduling next daily alarm: ... source=ROLLOVER_ON_FIRE"
echo " DNP-NOTIFY: Scheduling alarm: triggerTime=<tomorrow>"
echo " DNP-SCHEDULE: Scheduling OS alarm: variant=ALARM_CLOCK, action=com.timesafari.daily.NOTIFICATION, ..."
echo ""
print_info "Check logs for rollover scheduling errors:"
print_info " adb logcat -d | grep 'DNP-SCHEDULE\|DNP-NOTIFY' | tail -20"
print_info " adb shell dumpsys alarm | grep -A 3 'com.timesafari.dailynotification'"
echo ""
print_info "Manual verification:"
echo " If dumpsys shows a single RTC_WAKEUP alarm with"
echo " tag=*walarm*:com.timesafari.daily.NOTIFICATION for tomorrow,"
echo " treat TEST 0 as manually PASSED and update the script expectations later."
fi
wait_for_user