Files
daily-notification-plugin/test-apps/android-test-app/test-phase2.sh
Matthew Raymer fc2f64bae3 fix(notify): eliminate duplicate alarm scheduling and fix test harness counting
Centralize all notification alarm scheduling through NotifyReceiver.scheduleExactNotification()
with idempotence checks to prevent duplicate alarms. Implement one-alarm policy using
setAlarmClock() only. Fix test harness alarm counting to deduplicate by Alarm handle.

Plugin Changes:
- Add ScheduleSource enum to track scheduling paths (INITIAL_SETUP, ROLLOVER_ON_FIRE, etc.)
- Add DB-level idempotence check before scheduling (prevents logical duplicates)
- Add explicit alarm cancellation before scheduling (safety net)
- Implement one-alarm policy: use setAlarmClock() only, no setExact* fallbacks for same event
- Add deep logging for all AlarmManager calls (variant, requestCode, pendingIntentHash)
- Update all rollover paths (DailyNotificationReceiver, DailyNotificationWorker) to use
  centralized function with ROLLOVER_ON_FIRE source
- Add @JvmStatic annotation to scheduleExactNotification for Java interop

Test Harness Changes:
- Fix get_plugin_alarm_count() to deduplicate by Alarm handle (prevents double-counting
  same alarm in main list and "Next wake from idle" section)
- Update TEST 0 messaging: treat 0 alarms as race condition (inconclusive, not failure)
- Make post-rollover check the authoritative assertion point (only fails on >1 or 0 alarms)
- Remove redundant "Found 0 alarms - test may not be accurate" messages

This fixes the duplicate alarm bug where two distinct AlarmManager entries were created
for the same daily notification, violating the "one notification per day" contract.
2025-12-01 10:09:54 +00:00

347 lines
11 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
# ========================================
# Phase 2 Testing Script Force Stop Recovery
# ========================================
# Source shared library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/alarm-test-lib.sh"
# Phase 2 specific configuration
# Log tags / patterns (matched to actual ReactivationManager logs)
FORCE_STOP_SCENARIO_VALUE="FORCE_STOP"
COLD_START_SCENARIO_VALUE="COLD_START"
NONE_SCENARIO_VALUE="NONE"
BOOT_SCENARIO_VALUE="BOOT"
# Allow selecting specific tests on the command line (e.g. ./test-phase2.sh 2 3)
SELECTED_TESTS=()
# ------------------------------------------------------------------------------
# TEST 1 Force Stop with Cleared Alarms
# ------------------------------------------------------------------------------
test1_force_stop_cleared_alarms() {
section "TEST 1: Force Stop Alarms Cleared"
echo "Purpose: Verify force stop detection and alarm rescheduling when alarms are cleared."
pause
substep "Step 1: Launch app & check plugin status"
launch_app
ui_prompt "In the app UI, verify plugin status:\n\n ⚙️ Plugin Settings: ✅ Configured\n 🔌 Native Fetcher: ✅ Configured\n\nIf either shows ❌ or 'Not configured', click 'Configure Plugin', wait until both are ✅, then press Enter."
ui_prompt "Now schedule at least one future notification (e.g., click 'Test Notification' to schedule for a few minutes in the future)."
substep "Step 2: Verify alarms are scheduled"
show_alarms
local before_count system_count
before_count="$(get_plugin_alarm_count)"
system_count="$(get_system_alarm_count)"
info "Plugin alarms before force stop: $before_count (expected: 1)"
info "System/other alarms: $system_count (for context)"
if [[ "$before_count" -eq 0 ]]; then
warn "No plugin alarms found before force stop; TEST 1 may not be meaningful."
elif [[ "$before_count" -eq 1 ]]; then
ok "Single plugin alarm confirmed (one per day)"
else
warn "Found $before_count plugin alarms (expected: 1)"
fi
pause
substep "Step 3: Force stop app (should clear alarms on many devices)"
force_stop_app
substep "Step 4: Check alarms after force stop"
local after_count system_after
after_count="$(get_plugin_alarm_count)"
system_after="$(get_system_alarm_count)"
info "Plugin alarms after force stop: $after_count (expected: 0)"
info "System/other alarms: $system_after (for context)"
show_alarms
if [[ "$after_count" -gt 0 ]]; then
warn "Plugin alarms still present after force stop. This device/OS may not clear alarms on force stop."
warn "TEST 1 will continue but may not fully validate FORCE_STOP scenario."
fi
pause
substep "Step 5: Launch app (triggers recovery) and capture logs"
clear_logs
launch_app
sleep 5 # give recovery a moment to run
info "Collecting recovery logs..."
local logs
logs="$(get_recovery_logs)"
echo "$logs"
local scenario rescheduled verified errors
scenario="$(extract_scenario_from_logs "$logs")"
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
verified="$(extract_field_from_logs "$logs" "verified")"
errors="$(extract_field_from_logs "$logs" "errors")"
echo
info "Parsed recovery summary:"
echo " scenario = ${scenario:-<none>}"
echo " rescheduled= ${rescheduled}"
echo " verified = ${verified}"
echo " errors = ${errors}"
echo
if [[ "$errors" -gt 0 ]]; then
error "Recovery reported errors>0 (errors=$errors)"
fi
if [[ "$scenario" == "$FORCE_STOP_SCENARIO_VALUE" && "$rescheduled" -gt 0 ]]; then
ok "TEST 1 PASSED: Force stop detected and alarms rescheduled (scenario=$scenario, rescheduled=$rescheduled)."
elif [[ "$scenario" == "$FORCE_STOP_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
warn "TEST 1: scenario=FORCE_STOP but rescheduled=0. Check implementation or logs."
elif [[ "$after_count" -gt 0 ]]; then
info "TEST 1: Device/emulator kept alarms after force stop; FORCE_STOP scenario may not trigger here."
if [[ "$rescheduled" -gt 0 ]]; then
info "Recovery still worked (rescheduled=$rescheduled), but scenario was ${scenario:-COLD_START} instead of FORCE_STOP"
fi
else
warn "TEST 1: Expected FORCE_STOP scenario not clearly detected. Review logs and scenario detection logic."
info "Scenario detected: ${scenario:-<none>}, rescheduled=$rescheduled"
fi
substep "Step 6: Verify alarms are rescheduled in AlarmManager"
show_alarms
}
# ------------------------------------------------------------------------------
# TEST 2 Force Stop / Process Stop with Intact Alarms
# ------------------------------------------------------------------------------
test2_force_stop_intact_alarms() {
section "TEST 2: Force Stop / Process Stop Alarms Intact"
echo "Purpose: Verify that heavy FORCE_STOP recovery does not run when alarms are still present."
pause
substep "Step 1: Launch app & schedule notifications"
launch_app
ui_prompt "In the app UI, ensure plugin is configured and schedule at least one future notification.\n\nPress Enter when done."
substep "Step 2: Verify alarms are scheduled"
show_alarms
local before system_before
before="$(get_plugin_alarm_count)"
system_before="$(get_system_alarm_count)"
info "Plugin alarms before stop: $before (expected: 1)"
info "System/other alarms: $system_before (for context)"
if [[ "$before" -eq 0 ]]; then
warn "No plugin alarms found; TEST 2 may not be meaningful."
elif [[ "$before" -eq 1 ]]; then
ok "Single plugin alarm confirmed (one per day)"
else
warn "Found $before plugin alarms (expected: 1)"
fi
pause
substep "Step 3: Simulate a 'soft' stop or process kill that does NOT clear alarms"
info "Killing app process (non-destructive - may not clear alarms)..."
$ADB_BIN shell am kill "$APP_ID" || true
sleep 2
ok "Kill signal sent (soft stop)"
substep "Step 4: Verify alarms are still scheduled"
local after system_after
after="$(get_plugin_alarm_count)"
system_after="$(get_system_alarm_count)"
info "Plugin alarms after soft stop: $after (expected: 1)"
info "System/other alarms: $system_after (for context)"
show_alarms
if [[ "$after" -eq 0 ]]; then
warn "Alarms appear cleared after soft stop; this environment may not distinguish TEST 2 well."
fi
pause
substep "Step 5: Relaunch app and check recovery logs"
clear_logs
launch_app
sleep 5
info "Collecting recovery logs..."
local logs
logs="$(get_recovery_logs)"
echo "$logs"
local scenario rescheduled missed verified errors
scenario="$(extract_scenario_from_logs "$logs")"
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
missed="$(extract_field_from_logs "$logs" "missed")"
verified="$(extract_field_from_logs "$logs" "verified")"
errors="$(extract_field_from_logs "$logs" "errors")"
echo
info "Parsed recovery summary:"
echo " scenario = ${scenario:-<none>}"
echo " rescheduled= ${rescheduled}"
echo " missed = ${missed}"
echo " verified = ${verified}"
echo " errors = ${errors}"
echo
if [[ "$errors" -gt 0 ]]; then
error "Recovery reported errors>0 (errors=$errors)"
fi
if [[ "$after" -gt 0 && "$rescheduled" -eq 0 && "$scenario" != "$FORCE_STOP_SCENARIO_VALUE" ]]; then
ok "TEST 2 PASSED: Alarms remained intact, and FORCE_STOP scenario did not run (scenario=$scenario, rescheduled=0)."
else
warn "TEST 2: Verify that FORCE_STOP recovery didn't misfire when alarms were intact."
info "Scenario=${scenario:-<none>}, rescheduled=$rescheduled, after_count=$after"
fi
}
# ------------------------------------------------------------------------------
# TEST 3 First Launch / Empty DB Safeguard
# ------------------------------------------------------------------------------
test3_first_launch_no_schedules() {
section "TEST 3: First Launch / No Schedules Safeguard"
echo "Purpose: Ensure force-stop recovery is NOT triggered when DB is empty or plugin isn't configured."
pause
substep "Step 1: Uninstall app to clear DB/state"
set +e
$ADB_BIN uninstall "$APP_ID" >/dev/null 2>&1
set -e
ok "App uninstalled (state cleared)"
substep "Step 2: Reinstall app"
if $ADB_BIN install -r "$APK_PATH"; then
ok "App installed"
else
error "Reinstall failed"
exit 1
fi
info "Clearing logcat..."
$ADB_BIN logcat -c
ok "Logs cleared"
pause
substep "Step 3: Launch app for the first time"
launch_app
sleep 5
substep "Step 4: Collect logs and ensure no force-stop recovery ran"
local logs
logs="$(get_recovery_logs)"
echo "$logs"
local scenario rescheduled
scenario="$(extract_scenario_from_logs "$logs")"
rescheduled="$(extract_field_from_logs "$logs" "rescheduled")"
echo
info "Parsed summary:"
echo " scenario = ${scenario:-<none>}"
echo " rescheduled= ${rescheduled}"
echo
if [[ -z "$logs" ]]; then
ok "TEST 3 PASSED: No force-stop recovery logs on first launch."
elif [[ "$scenario" == "$NONE_SCENARIO_VALUE" && "$rescheduled" -eq 0 ]]; then
ok "TEST 3 PASSED: NONE scenario logged with rescheduled=0 on first launch."
elif [[ "$rescheduled" -gt 0 ]]; then
warn "TEST 3: rescheduled>0 on first launch / empty DB. Check that force-stop recovery isn't misfiring."
else
info "TEST 3: Logs present but no rescheduling; review scenario handling to ensure it's explicit about NONE / FIRST_LAUNCH."
fi
}
# ------------------------------------------------------------------------------
# Main
# ------------------------------------------------------------------------------
main() {
# Allow selecting specific tests: e.g. `./test-phase2.sh 1 3`
if [[ "$#" -gt 0 && ( "$1" == "-h" || "$1" == "--help" ) ]]; then
echo "Usage: $0 [TEST_IDS...]"
echo
echo "If no TEST_IDS are given, all tests (1, 2, 3) will run."
echo "Examples:"
echo " $0 # run all tests"
echo " $0 1 # run only TEST 1"
echo " $0 2 3 # run only TEST 2 and TEST 3"
return 0
fi
SELECTED_TESTS=("$@")
echo
echo "========================================"
echo "Phase 2 Testing Script Force Stop Recovery"
echo "========================================"
echo
echo "This script will guide you through Phase 2 tests."
echo "You'll be prompted when UI interaction is needed."
echo
pause
require_adb_device
build_app
install_app
if should_run_test "1" SELECTED_TESTS; then
test1_force_stop_cleared_alarms
pause
fi
if should_run_test "2" SELECTED_TESTS; then
test2_force_stop_intact_alarms
pause
fi
if should_run_test "3" SELECTED_TESTS; then
test3_first_launch_no_schedules
fi
section "Testing Complete"
echo "Test Results Summary (see logs above for details):"
echo
echo "TEST 1: Force Stop Alarms Cleared"
echo " - Check logs for scenario=$FORCE_STOP_SCENARIO_VALUE and rescheduled>0"
echo
echo "TEST 2: Force Stop / Process Stop Alarms Intact"
echo " - Verify FORCE_STOP scenario is not incorrectly triggered when alarms are still present"
echo
echo "TEST 3: First Launch / No Schedules"
echo " - Confirm that no force-stop recovery runs, or that NONE/FIRST_LAUNCH scenario is logged with rescheduled=0"
echo
ok "Phase 2 testing script complete!"
echo
echo "Next steps:"
echo " - Review logs above"
echo " - Capture snippets into PHASE2-EMULATOR-TESTING.md"
echo " - Update PHASE2-VERIFICATION.md and unified directive status matrix"
echo
}
main "$@"