Files
daily-notification-plugin/test-apps/android-test-app/test-phase1.sh
Matthew Raymer b53042d679 test: improve rollover detection and UI auto-refresh
- Normalize alarm time seconds to :00 for consistent comparison
- Compare dates (YYYY-MM-DD) instead of full timestamps to detect rollover
- Expand logcat search patterns to catch all rollover logs (DN|RESCHEDULE, etc.)
- Add 5-second wait after notification fire to allow rollover processing
- UI: Normalize seconds display to :00 in all time displays
- UI: Add auto-refresh mechanism that detects nextNotificationTime changes
- UI: Poll every 3 seconds and force refresh when rollover detected
- UI: Initialize tracking variable on page load for change detection

Fixes issue where test passed but alarm time didn't actually change,
and UI wasn't updating to show rescheduled notification time after rollover.
2025-12-29 09:36:19 +00:00

1698 lines
67 KiB
Bash
Executable File

#!/usr/bin/env bash
# Phase 1 Testing Script - Interactive Test Runner
# Guides through all Phase 1 tests with clear prompts for UI interaction
# Strict mode (P0.2)
set -euo pipefail
IFS=$'\n\t'
# Source shared library
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/alarm-test-lib.sh"
# Initialize run directory (P1)
ensure_run_dir || {
error "Failed to initialize run directory"
exit 1
}
# Phase 1 specific configuration
VERIFY_FIRE=${VERIFY_FIRE:-false} # Set VERIFY_FIRE=true to enable alarm fire verification
APP_DIR="${SCRIPT_DIR}"
PROJECT_ROOT="$(cd "${APP_DIR}/../.." && pwd)"
# Allow selecting specific tests on the command line (e.g. ./test-phase1.sh 1 2)
SELECTED_TESTS=()
check_plugin_configured() {
print_info "Checking if plugin is already configured..."
# Wait a moment for app to fully load
sleep 3
# Check if database exists (indicates plugin has been used)
DB_EXISTS=$($ADB_BIN shell run-as "${APP_ID}" ls databases/ 2>/dev/null | grep -c "daily_notification" || echo "0")
# Check if SharedPreferences has configuration (more reliable)
# The plugin stores config in SharedPreferences
PREFS_EXISTS=$($ADB_BIN shell run-as "${APP_ID}" ls shared_prefs/ 2>/dev/null | grep -c "DailyNotification" || echo "0")
# Check recent logs for configuration activity
RECENT_CONFIG=$($ADB_BIN logcat -d -t 50 | grep -E "Plugin configured|configurePlugin|Configuration" | tail -3)
if [ "${DB_EXISTS}" -gt "0" ] || [ "${PREFS_EXISTS}" -gt "0" ]; then
print_success "Plugin appears to be configured (database or preferences exist)"
# Show user what to check in UI
print_info "Please verify in the app UI that you see:"
echo " ⚙️ Plugin Settings: ✅ Configured"
echo " 🔌 Native Fetcher: ✅ Configured"
echo ""
echo "If both show ✅, the plugin is configured and you can skip configuration."
echo "If either shows ❌ or 'Not configured', you'll need to click 'Configure Plugin'."
echo ""
return 0
else
print_info "Plugin not configured (no database or preferences found)"
print_info "You will need to click 'Configure Plugin' in the app UI"
return 1
fi
}
check_permissions() {
print_info "Checking notification permissions..."
# Check if POST_NOTIFICATIONS permission is granted (Android 13+)
# Strip whitespace/newlines to ensure we get a single integer
PERM_CHECK=$($ADB_BIN shell dumpsys package "${APP_ID}" | grep -A 5 "granted=true" | grep -c "android.permission.POST_NOTIFICATIONS" 2>/dev/null | tr -d '\n\r ' || echo "0")
# Also check via app's permission status if available
PERM_GRANTED=false
# Ensure PERM_CHECK is a valid integer (strip any remaining non-digits)
PERM_CHECK=$(echo "${PERM_CHECK}" | tr -d '\n\r ' | grep -oE '^[0-9]+$' || echo "0")
if [ "${PERM_CHECK}" -gt "0" ] 2>/dev/null; then
PERM_GRANTED=true
else
# Check if we're on Android 12 or below (permission not required)
SDK_VERSION=$($ADB_BIN shell getprop ro.build.version.sdk 2>/dev/null | tr -d '\n\r ' || echo "0")
SDK_VERSION=$(echo "${SDK_VERSION}" | grep -oE '^[0-9]+$' || echo "0")
if [ "${SDK_VERSION}" -lt "33" ] 2>/dev/null; then
PERM_GRANTED=true # Pre-Android 13 doesn't need runtime permission
fi
fi
if [ "${PERM_GRANTED}" = true ]; then
print_success "Notification permissions granted"
return 0
else
print_warn "Notification permissions may not be granted"
return 1
fi
}
# Comprehensive prerequisite verification (aligned with PHASE1_TEST0_GOLDEN.md)
# Verifies all 5 items specified in the golden run document:
# 1. Plugin Settings: Configured
# 2. Native Fetcher: Configured
# 3. Notifications: Granted
# 4. Exact Alarms: Granted
# 5. Channel: Enabled (High)
#
# Usage: verify_all_prerequisites "phase1" "phase1_test0" "p1_t0_s2"
# This will verify all prerequisites and emit step events for each check
verify_all_prerequisites() {
local phase_id="${1:-}"
local test_id="${2:-}"
local base_step_id="${3:-}"
info "Verifying all prerequisites (aligned with golden run specification)..."
local all_ok=true
local missing_items=()
# 1. Check Plugin Settings: Configured
local step_id="${base_step_id}_plugin"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Plugin Settings: Configured"
if check_plugin_configured >/dev/null 2>&1; then
ok "Plugin Settings: ✅ Configured"
step_pass "${step_id}" "Plugin Settings configured"
else
warn "Plugin Settings: ❌ Not configured"
all_ok=false
missing_items+=("Plugin Settings")
step_fail "${step_id}" "Plugin Settings not configured"
fi
# 2. Check Native Fetcher: Configured
# (Plugin config check also verifies native fetcher via SharedPreferences)
# For explicit verification, we check logs for fetcher configuration
step_id="${base_step_id}_fetcher"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Native Fetcher: Configured"
local fetcher_configured=false
local fetcher_logs=$($ADB_BIN logcat -d -t 100 | grep -E "Native fetcher.*registered|FETCHER.*CONFIGURE|configureNativeFetcher" | tail -5 || true)
if [ -n "${fetcher_logs}" ] || check_plugin_configured >/dev/null 2>&1; then
ok "Native Fetcher: ✅ Configured"
step_pass "${step_id}" "Native Fetcher configured"
fetcher_configured=true
else
warn "Native Fetcher: ⚠️ Status unclear (check UI)"
step_warn "${step_id}" "Native Fetcher status unclear - verify in UI"
# Don't fail on this - let UI verification catch it
fi
# 3. Check Notifications: Granted
step_id="${base_step_id}_notifications"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Notifications: Granted"
if check_permissions >/dev/null 2>&1; then
ok "Notifications: ✅ Granted"
step_pass "${step_id}" "Notifications permission granted"
else
error "Notifications: ❌ Not granted"
all_ok=false
missing_items+=("Notifications permission")
step_fail "${step_id}" "Notifications permission not granted"
fi
# 4. Check Exact Alarms: Granted
# Best way: Check if app has alarms with exactAllowReason=policy_permission
# This indicates the permission is granted and working
step_id="${base_step_id}_exact_alarms"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Exact Alarms: Granted"
local exact_alarm_granted=false
local sdk_version=$($ADB_BIN shell getprop ro.build.version.sdk 2>/dev/null | tr -d '\n\r ' | grep -oE '^[0-9]+$' || echo '0')
if [ "${sdk_version}" -ge "31" ] 2>/dev/null; then
# Android 12+ requires SCHEDULE_EXACT_ALARM permission
# Check if app has any alarms with exactAllowReason=policy_permission
# This is the most reliable indicator that exact alarms are working
local alarm_dump=$($ADB_BIN shell dumpsys alarm 2>/dev/null | grep -A 10 "${APP_ID}" | grep -c "exactAllowReason=policy_permission" || echo "0")
alarm_dump=$(echo "${alarm_dump}" | tr -d '\n\r ' | grep -oE '^[0-9]+$' || echo "0")
if [ "${alarm_dump}" -gt "0" ] 2>/dev/null; then
# Found alarms with policy_permission - exact alarms are working
exact_alarm_granted=true
else
# No alarms yet, but check if permission is declared in manifest
# (We can't easily check runtime permission status via ADB, so we'll rely on UI)
local has_perm=$($ADB_BIN shell dumpsys package "${APP_ID}" | grep -c "android.permission.SCHEDULE_EXACT_ALARM" 2>/dev/null | tr -d '\n\r ' || echo "0")
has_perm=$(echo "${has_perm}" | grep -oE '^[0-9]+$' || echo "0")
if [ "${has_perm}" -gt "0" ] 2>/dev/null; then
# Permission is declared, assume it's granted (will be verified in UI)
exact_alarm_granted=true
fi
fi
else
# Pre-Android 12 doesn't require this permission
exact_alarm_granted=true
fi
if [ "${exact_alarm_granted}" = true ]; then
ok "Exact Alarms: ✅ Granted"
step_pass "${step_id}" "Exact Alarms permission granted"
else
warn "Exact Alarms: ⚠️ May not be granted (check UI)"
step_warn "${step_id}" "Exact Alarms permission unclear - verify in UI"
# Don't fail - let UI verification catch it
fi
# 5. Check Channel: Enabled (High)
# Channel status is best verified via UI, but we can check logs
step_id="${base_step_id}_channel"
set_test_context "${phase_id}" "${test_id}" "${step_id}"
step_start "${step_id}" "Verifying Channel: Enabled (High)"
local channel_logs=$($ADB_BIN logcat -d -t 100 | grep -E "Channel.*enabled|channel.*importance|NotificationChannel" | tail -5 || true)
if [ -n "${channel_logs}" ]; then
ok "Channel: ✅ Status found in logs (verify 'Enabled (High)' in UI)"
step_pass "${step_id}" "Channel status verified"
else
info "Channel: ⚠️ Verify 'Enabled (High)' status in UI"
step_warn "${step_id}" "Channel status unclear - verify 'Enabled (High)' in UI"
# Don't fail - channel is created automatically
fi
# Final UI verification prompt (aligned with golden run step 4)
if [ "${MODE}" != "ci" ]; then
echo ""
info "=== Final UI Verification (Golden Run Step 4) ==="
info "Please confirm in the app UI that ALL of the following show ✅:"
echo " ⚙️ Plugin Settings: ✅ Configured"
echo " 🔌 Native Fetcher: ✅ Configured"
echo " 🔔 Notifications: ✅ Granted"
echo " ⏰ Exact Alarms: ✅ Granted"
echo " 📢 Channel: ✅ Enabled (High)"
echo ""
if [ "${all_ok}" = false ]; then
error "Some prerequisites are missing: ${missing_items[*]}"
ui_prompt "Please fix the missing prerequisites in the app UI, then press Enter to continue."
else
ui_prompt "If all 5 items show ✅ in the UI, press Enter to continue. Otherwise, fix any issues first."
fi
fi
if [ "${all_ok}" = false ] && [ "${MODE}" = "ci" ]; then
error "Prerequisites not met: ${missing_items[*]}"
return 1
fi
return 0
}
ensure_permissions() {
if check_permissions; then
print_success "Permissions already granted"
return 0
else
print_info "Notification permissions needed"
wait_for_ui_action "In the app UI, click the 'Request Permissions' button.
This will show a system permission dialog.
Steps:
1. Click 'Request Permissions' button
2. In the system dialog, tap 'Allow' to grant notification permission
3. Return to the app and verify the status shows:
- 🔔 Notifications: ✅ Granted (or similar)
Once permission is granted, press Enter to continue."
# Re-check permissions
sleep 2
if check_permissions; then
print_success "Permissions granted"
return 0
else
print_warn "Permissions may still not be granted - continuing anyway"
print_info "If notifications fail, you may need to grant permissions manually"
return 1
fi
fi
}
ensure_plugin_configured() {
if check_plugin_configured; then
# Plugin might be configured, but let user verify
wait_for_ui_action "Please check the Plugin Status section at the top of the app.
If you see:
- ⚙️ Plugin Settings: ✅ Configured
- 🔌 Native Fetcher: ✅ Configured
- 🔔 Notifications: ✅ Granted (or similar)
Then the plugin is already configured - just press Enter to continue.
If any show ❌ or 'Not configured':
- Click 'Request Permissions' if notifications are not granted
- Click 'Configure Plugin' if settings/fetcher are not configured
- Wait for all to show ✅, then press Enter."
# Give a moment for any configuration that just happened
sleep 2
print_success "Continuing with tests (plugin configuration verified or skipped)"
return 0
else
# Plugin definitely needs configuration
print_info "Plugin needs configuration"
# First ensure permissions
ensure_permissions
wait_for_ui_action "Click the 'Configure Plugin' button in the app UI.
Wait for the status to update:
- ⚙️ Plugin Settings: Should change to ✅ Configured
- 🔌 Native Fetcher: Should change to ✅ Configured
Once both show ✅, press Enter to continue."
# Verify configuration completed
sleep 2
print_success "Plugin configuration completed (or verified)"
fi
}
# kill_app is now provided by the library, but Phase 1 uses it with PACKAGE variable
# The library version uses APP_ID, which is set to PACKAGE, so it should work
# But we keep this as a compatibility wrapper if needed
# Phase 1 specific helper: get_current_time (used for fire verification)
get_current_time() {
$ADB_BIN shell date +%s
}
# ========================================
# CLI Mode Parsing (P2.1)
# ========================================
parse_cli_args() {
local mode="all"
SELECTED_TESTS=()
while [[ $# -gt 0 ]]; do
case "$1" in
--setup)
mode="setup"
shift
;;
--run)
mode="run"
shift
;;
--smoke)
mode="smoke"
shift
;;
--all)
mode="all"
shift
;;
--ci)
mode="ci"
shift
;;
-h|--help)
show_help
exit 0
;;
[0-9]*)
SELECTED_TESTS+=("$1")
shift
;;
*)
error "Unknown option: $1"
show_help
exit 1
;;
esac
done
echo "$mode"
}
show_help() {
cat <<EOF
Usage: $0 [MODE] [TEST_IDS...]
Modes:
--setup Run setup only (permissions, app install, plugin config checks)
--run Run actual Phase 1 tests (requires setup first)
--smoke Minimal test: schedule one notification + verify pending
--all Run setup + all tests (default)
--ci Non-interactive: automated checks only, fail fast
Test Selection:
[TEST_IDS] Optional test numbers to run (e.g., 0 1 2)
If omitted, all tests run
Examples:
$0 # Run all (setup + tests)
$0 --setup # Setup only
$0 --run # Run tests only
$0 --smoke # Quick smoke test
$0 --ci # CI mode (non-interactive)
$0 --run 1 2 # Run only tests 1 and 2
$0 --smoke 0 # Run smoke test for test 0
EOF
}
# ========================================
# Setup Mode (P2.1)
# ========================================
run_setup() {
section "Phase 1 Setup"
info "This mode performs pre-flight checks and setup:"
echo " • ADB device connection"
echo " • Emulator readiness"
echo " • App build and installation"
echo " • Permission checks"
echo " • Plugin configuration checks"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Pre-flight checks
section "Pre-Flight Checks"
require_adb_device
# Build and install
build_app
install_app
# Clear logs
clear_logs
# Launch app for setup checks
launch_app
sleep 2
# Check permissions and plugin configuration together
section "Permissions & Plugin Configuration"
info "Checking both permissions and plugin/fetcher configuration..."
echo ""
# Check both silently first
local perms_ok=false
local plugin_ok=false
if check_permissions >/dev/null 2>&1; then
perms_ok=true
fi
if check_plugin_configured >/dev/null 2>&1; then
plugin_ok=true
fi
# Show status
if [ "${perms_ok}" = "true" ]; then
ok "Notification permissions: ✅ Granted"
else
warn "Notification permissions: ❌ Not granted"
fi
if [ "${plugin_ok}" = "true" ]; then
ok "Plugin configuration: ✅ Configured"
else
warn "Plugin configuration: ❌ Not configured"
fi
# If either needs attention, show single combined prompt
if [ "${MODE}" = "ci" ]; then
if [ "${perms_ok}" != "true" ]; then
error "Permissions not granted - cannot proceed in CI mode"
exit 1
fi
if [ "${plugin_ok}" != "true" ]; then
error "Plugin not configured - cannot proceed in CI mode"
exit 1
fi
else
# Show single combined prompt for both
if [ "${perms_ok}" != "true" ] || [ "${plugin_ok}" != "true" ]; then
ui_prompt "Please configure in the app UI:
REQUIRED STATUS (all must show ✅):
1) 🔔 Notifications: ✅ Granted
2) ⚙️ Plugin Settings: ✅ Configured
3) 🔌 Native Fetcher: ✅ Configured
ACTIONS:
- If Notifications shows ❌: Click 'Request Permissions' button
- If Plugin Settings or Native Fetcher show ❌: Click 'Configure Plugin' button
- Wait for all three to show ✅
Once all three show ✅, press Enter to continue."
# Re-check both after user action
sleep 2
echo ""
info "Re-checking after configuration..."
perms_ok=false
plugin_ok=false
if check_permissions >/dev/null 2>&1; then
perms_ok=true
ok "Notification permissions: ✅ Verified"
else
warn "Notification permissions: ⚠️ Still not granted"
fi
if check_plugin_configured >/dev/null 2>&1; then
plugin_ok=true
ok "Plugin configuration: ✅ Verified"
else
warn "Plugin configuration: ⚠️ Still not configured"
fi
echo ""
if [ "${perms_ok}" = "true" ] && [ "${plugin_ok}" = "true" ]; then
ok "All checks passed - ready to proceed"
else
warn "Some items may still need configuration - continuing anyway"
info "If tests fail, verify all three items show ✅ in the app UI"
fi
else
# Both are OK, but still prompt for final verification
ui_prompt "Please verify in the app UI that you see:
1) 🔔 Notifications: ✅ Granted
2) ⚙️ Plugin Settings: ✅ Configured
3) 🔌 Native Fetcher: ✅ Configured
If all show ✅, press Enter to continue.
If any show ❌, configure them first, then press Enter."
fi
fi
ok "Setup complete!"
# Capture initial state
capture_alarms "setup_initial"
capture_logcat "setup_initial" "DNP" 100
evidence_block "phase1_setup"
}
# ========================================
# Smoke Test Mode (P2.1)
# ========================================
run_smoke_test() {
section "Phase 1 Smoke Test"
info "Minimal test: schedule one notification + verify pending"
echo "Expected time: 2-4 minutes"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Ensure setup is done
if ! check_permissions >/dev/null 2>&1; then
warn "Setup not complete - running setup first..."
run_setup
fi
# Capture before state
capture_alarms "smoke_before"
# Schedule notification
section "Smoke Test: Schedule Notification"
launch_app
if [ "${MODE}" != "ci" ]; then
ui_prompt "1) In the app UI, click 'Test Notification' to schedule a notification for 4 minutes in the future."
else
info "CI mode: Assuming notification is scheduled manually or via automation"
sleep 3
fi
# Wait for alarm to be registered
sleep 3
# Capture after state
capture_alarms "smoke_after"
capture_logcat "smoke_after" "DNP-SCHEDULE|DNP-NOTIFY" 50
# Verify alarm exists
local alarm_count
alarm_count="$(get_plugin_alarm_count)"
if [ "${alarm_count}" -eq "1" ]; then
verdict_pass "phase1_smoke" "Smoke test passed: 1 alarm scheduled"
elif [ "${alarm_count}" -gt "1" ]; then
verdict_fail "phase1_smoke" "Smoke test failed: ${alarm_count} alarms found (expected: 1)"
else
verdict_warn "phase1_smoke" "Smoke test inconclusive: ${alarm_count} alarms found (expected: 1, may be race condition)"
fi
evidence_block "phase1_smoke"
}
# ========================================
# Main Test Execution
# ========================================
main() {
# Parse CLI arguments
MODE="$(parse_cli_args "$@")"
section "Phase 1 Testing Script"
echo "Mode: ${MODE}"
echo "Run ID: ${RUN_ID}"
echo "Evidence directory: $(get_run_dir)"
echo ""
if [ "${MODE}" = "setup" ]; then
run_setup
return 0
elif [ "${MODE}" = "smoke" ]; then
run_smoke_test
return 0
elif [ "${MODE}" = "ci" ]; then
info "CI mode: Non-interactive, automated checks only"
run_setup
# In CI mode, we might want to run a subset of tests
# For now, just run smoke test
run_smoke_test
return 0
fi
# For --run and --all modes, continue with full test suite
if [ "${MODE}" = "all" ]; then
info "Running full test suite (setup + tests)"
if [ "${MODE}" != "ci" ]; then
pause
fi
run_setup
elif [ "${MODE}" = "run" ]; then
info "Running tests only (assuming setup already done)"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi
# ============================================
# TEST 0: Daily Rollover (Core Contract Verification)
# ============================================
if should_run_test "0" SELECTED_TESTS; then
section "TEST 0: Daily Rollover Verification"
# Set test context for event emission
set_test_context "phase1" "phase1_test0" ""
info "Purpose: Verify that after a notification fires, the next day's"
info " schedule is correctly computed and only ONE alarm exists."
info ""
info "Expected time: 5-10 minutes"
info "Automatable: Partial (requires manual clock manipulation or waiting)"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
set_test_context "phase1" "phase1_test0" "p1_t0_s1"
step_start "p1_t0_s1" "Capturing initial state"
capture_alarms "test0_initial"
capture_logcat "test0_initial" "DNP" 50
step_pass "p1_t0_s1" "Initial state captured"
substep "Step 1: Verify setup and schedule a test notification"
set_test_context "phase1" "phase1_test0" "p1_t0_s2"
step_start "p1_t0_s2" "Verifying prerequisites and scheduling test notification"
launch_app
# Comprehensive prerequisite verification (aligned with PHASE1_TEST0_GOLDEN.md step 4)
# Verifies all 5 items: Plugin Settings, Native Fetcher, Notifications, Exact Alarms, Channel
verify_all_prerequisites "phase1" "phase1_test0" "p1_t0_s2"
# If prerequisites failed and we're in CI mode, exit
if [ $? -ne 0 ] && [ "${MODE}" = "ci" ]; then
error "Prerequisites not met - cannot proceed with Test 0"
exit 1
fi
# Capture pre-schedule state
capture_alarms "test0_before_schedule"
capture_screenshot "test0_before_schedule"
local initial_count system_count
initial_count="$(get_plugin_alarm_count)"
system_count="$(get_system_alarm_count)"
info "Current notification alarms: ${initial_count} (expected before scheduling: 0)"
info "System/other alarms: ${system_count} (for context)"
if [ "${initial_count}" -eq "0" ] 2>/dev/null; then
ok "No existing notification alarms (clean state)"
if [ "${MODE}" != "ci" ]; then
ui_prompt "1) In the app UI, schedule a daily notification.
For this test, schedule it for a time very soon (e.g., 1-2 minutes from now)
to observe the rollover behavior.
This will schedule:
• 1 notification alarm (AlarmManager) for the specified time
• 1 prefetch job (WorkManager) for 2 minutes before that time"
else
info "CI mode: Assuming notification is scheduled manually"
sleep 3
fi
sleep 3 # Give alarm time to be registered
# Capture post-schedule state
capture_alarms "test0_after_schedule"
capture_screenshot "test0_after_schedule"
local post_schedule_count
post_schedule_count="$(get_plugin_alarm_count)"
# Preliminary check (final verdict comes after rollover)
if [ "${post_schedule_count}" -eq "0" ] 2>/dev/null; then
warn "Found 0 plugin alarms right after scheduling (may be race condition)"
info "Alarm may not be visible in dumpsys yet - will verify after rollover"
elif [ "${post_schedule_count}" -eq "1" ] 2>/dev/null; then
ok "Found 1 notification alarm (expected: 1) - preliminary check passed"
else
warn "Found ${post_schedule_count} notification alarms (expected: 1) - DUPLICATES DETECTED!"
error "This indicates duplicate NOTIFICATION alarms were created (BUG)"
show_plugin_alarms_compact
fi
initial_count="${post_schedule_count}"
fi
# Show alarm details if we found exactly 1 alarm
local initial_alarm_time=""
if [ "${initial_count}" -eq "1" ] 2>/dev/null; then
ok "Single notification alarm scheduled (one per day)"
local alarm_details
alarm_details="$($ADB_BIN shell dumpsys alarm | grep -A 3 "com.timesafari.dailynotification" | grep -A 3 "com.timesafari.daily.NOTIFICATION" | head -10)"
if [ -n "${alarm_details}" ]; then
info "Notification alarm details:"
echo "${alarm_details}" | head -5
echo ""
# Extract initial alarm time for rollover verification (normalize seconds to 00)
initial_alarm_time=$(echo "${alarm_details}" | grep -oE "origWhen=[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}" | head -1 | sed 's/origWhen=//' | sed 's/:[0-9][0-9]$/:00/')
if [ -n "${initial_alarm_time}" ]; then
info "Initial alarm time: ${initial_alarm_time} (normalized)"
fi
fi
elif [ "${initial_count}" -gt "1" ] 2>/dev/null; then
warn "Found ${initial_count} notification alarms (expected: 1) - DUPLICATES DETECTED!"
fi
step_pass "p1_t0_s2" "Notification scheduled"
substep "Step 2: Wait for notification to fire"
set_test_context "phase1" "phase1_test0" "p1_t0_s3"
step_start "p1_t0_s3" "Waiting for notification to fire"
info "The test will automatically verify rollover after the notification fires."
echo ""
echo "What will be verified automatically:"
echo " • Alarm count (should be exactly 1)"
echo " • Alarm time changed (should be 24 hours later)"
echo " • Rollover logs present"
echo ""
if [ "${MODE}" != "ci" ]; then
ui_prompt "Please wait for the notification to fire (or advance the emulator clock past the scheduled time).
The test will automatically verify:
• Only ONE alarm exists (one per day)
• Alarm time advanced to tomorrow (24 hours later)
• Rollover occurred (logs will be checked)
Press Enter after the notification has fired (or after advancing the clock)."
else
info "CI mode: Waiting for notification to fire..."
sleep 5 # Give time for rollover if automated
fi
step_pass "p1_t0_s3" "Notification fired (or time advanced)"
substep "Step 3: Verify rollover state"
set_test_context "phase1" "phase1_test0" "p1_t0_s4"
step_start "p1_t0_s4" "Waiting for rollover to complete"
info "Waiting for rollover to complete (notification should auto-schedule next day's alarm)..."
info "Allowing up to ~15 seconds for Android to process rollover and update alarms..."
# Wait longer for rollover to complete (notification fires → rollover schedules next day)
sleep 5 # Give Android time to process the notification fire and rollover
local post_rollover_count system_final
post_rollover_count="$(wait_for_stable_plugin_alarm_count 5 2)"
system_final="$(get_system_alarm_count)"
# Capture post-rollover state
capture_alarms "test0_after_rollover"
# Look for rollover logs: DN|RESCHEDULE, DN|DISPLAY, DN|RECEIVE, ROLLOVER_ON_FIRE, etc.
capture_logcat "test0_after_rollover" "DN|RESCHEDULE|DN|DISPLAY|DN|RECEIVE|ROLLOVER|DNP-SCHEDULE" 200
capture_screenshot "test0_after_rollover"
step_pass "p1_t0_s4" "Post-rollover evidence captured"
set_test_context "phase1" "phase1_test0" "p1_t0_s6"
step_start "p1_t0_s6" "Verifying schedule state"
info "Notification alarms after rollover: ${post_rollover_count} (expected: 1)"
info "System/other alarms: ${system_final} (for context)"
# Extract post-rollover alarm time and verify it changed
local post_rollover_alarm_time=""
local rollover_verified=false
if [ "${post_rollover_count}" -eq "1" ] 2>/dev/null; then
local post_alarm_details
post_alarm_details="$($ADB_BIN shell dumpsys alarm | grep -A 3 "com.timesafari.dailynotification" | grep -A 3 "com.timesafari.daily.NOTIFICATION" | head -10)"
if [ -n "${post_alarm_details}" ]; then
# Extract post-rollover alarm time (normalize seconds to 00 for comparison)
post_rollover_alarm_time=$(echo "${post_alarm_details}" | grep -oE "origWhen=[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}" | head -1 | sed 's/origWhen=//' | sed 's/:[0-9][0-9]$/:00/')
if [ -n "${post_rollover_alarm_time}" ]; then
info "Post-rollover alarm time: ${post_rollover_alarm_time} (normalized)"
# Verify alarm time changed (rollover occurred)
# Compare dates only (YYYY-MM-DD) to detect day change
if [ -n "${initial_alarm_time}" ] && [ -n "${post_rollover_alarm_time}" ]; then
local initial_date=$(echo "${initial_alarm_time}" | cut -d' ' -f1)
local post_date=$(echo "${post_rollover_alarm_time}" | cut -d' ' -f1)
if [ "${initial_date}" != "${post_date}" ]; then
ok "Alarm date changed: ${initial_alarm_time}${post_rollover_alarm_time}"
rollover_verified=true
else
warn "Alarm date did NOT change: ${post_rollover_alarm_time} (same date as initial: ${initial_date})"
warn "This indicates the notification did not fire and rollover did not occur"
rollover_verified=false
fi
fi
fi
fi
# Check for rollover logs (wider search including all possible rollover patterns)
# Look for: ROLLOVER_ON_FIRE source, RESCHEDULE logs, DNP-SCHEDULE with ROLLOVER, etc.
# Note: DN|RESCHEDULE uses pipe character, so we need to escape it properly
local rollover_logs=$($ADB_BIN logcat -d -t 1000 | grep -E "ROLLOVER_ON_FIRE|source=ROLLOVER|DNP-SCHEDULE.*ROLLOVER|DN\|RESCHEDULE|DN\|RESCHEDULE_OK|DN\|RESCHEDULE_START|Scheduling next.*notification|scheduleNextNotification|Next notification scheduled" | tail -15 || true)
if [ -n "${rollover_logs}" ]; then
ok "Rollover logs found - notification fired and rollover occurred"
info "Rollover log excerpt:"
echo "${rollover_logs}" | head -3 | sed 's/^/ /'
rollover_verified=true
else
warn "No rollover logs found - checking if notification fired at all..."
# Check for any notification display logs
local notification_logs=$($ADB_BIN logcat -d -t 500 | grep -E "DN\|DISPLAY_NOTIF|Notification.*displayed|com.timesafari.daily.NOTIFICATION" | tail -5 || true)
if [ -n "${notification_logs}" ]; then
warn "Notification display logs found, but no rollover logs - rollover may have failed"
info "Notification log excerpt:"
echo "${notification_logs}" | head -2 | sed 's/^/ /'
else
warn "No notification logs found - notification may not have fired"
fi
if [ "${rollover_verified}" = false ]; then
warn "⚠️ Rollover verification failed: alarm date unchanged AND no rollover logs"
fi
fi
fi
# Final verdict
if [ "${post_rollover_count}" -eq "1" ] 2>/dev/null && [ "${rollover_verified}" = true ]; then
step_pass "p1_t0_s6" "Schedule state verified - exactly 1 alarm, rollover confirmed"
set_test_context "phase1" "phase1_test0" "p1_t0_s7"
verdict_pass "test0_daily_rollover" "Daily rollover created exactly one NOTIFICATION alarm for tomorrow"
elif [ "${post_rollover_count}" -eq "1" ] 2>/dev/null && [ "${rollover_verified}" = false ]; then
step_warn "p1_t0_s6" "Alarm count correct but rollover not verified"
warn "Found 1 alarm but time did not change - notification may not have fired"
set_test_context "phase1" "phase1_test0" "p1_t0_s7"
verdict_warn "test0_daily_rollover" "Alarm count correct (1) but rollover not verified - alarm time unchanged, no rollover logs"
elif [ "${post_rollover_count}" -gt "1" ] 2>/dev/null; then
step_fail "p1_t0_s6" "Duplicate alarms detected"
error "Daily rollover created ${post_rollover_count} NOTIFICATION alarms (duplicates)"
show_plugin_alarms_compact
set_test_context "phase1" "phase1_test0" "p1_t0_s7"
verdict_fail "test0_daily_rollover" "Duplicate alarms detected after rollover (expected: 1, got: ${post_rollover_count})"
else
step_warn "p1_t0_s6" "No alarm found after rollover"
warn "No NOTIFICATION alarm found after rollover (expected: 1, got: 0)"
show_plugin_alarms_compact
set_test_context "phase1" "phase1_test0" "p1_t0_s7"
verdict_warn "test0_daily_rollover" "Rollover may have failed - no alarm found (expected: 1, got: 0)"
fi
evidence_block "test0_daily_rollover"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 0" block
# ============================================
# TEST 1: Force-Stop Recovery - Database Restoration
# ============================================
if should_run_test "1" SELECTED_TESTS; then
section "TEST 1: Force-Stop Recovery - Database Restoration"
# Set test context
set_test_context "phase1" "phase1_test1" ""
info "Purpose: Verify that after force-stop (which clears alarms), recovery"
info " uses the database to rebuild alarms on app relaunch."
info ""
info "Expected time: 5-8 minutes"
info "Automatable: Partial (requires manual force-stop verification)"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
set_test_context "phase1" "phase1_test1" "p1_t1_s1"
step_start "p1_t1_s1" "Clean start - verify no lingering alarms"
capture_alarms "test1_initial"
capture_logcat "test1_initial" "DNP" 50
local lingering_count system_count
lingering_count="$(get_plugin_alarm_count)"
system_count="$(get_system_alarm_count)"
info "Current plugin notification alarms: ${lingering_count}"
info "System/other alarms: ${system_count} (for context)"
local goto_test1_end=false
if [ "${lingering_count}" -gt "0" ] 2>/dev/null; then
warn "Found ${lingering_count} lingering plugin alarm(s) - these will interfere with TEST 1"
info "TEST 1 needs a clean state (no existing plugin alarms)"
info "Resetting app state via uninstall + reinstall..."
# Uninstall existing app
info "Uninstalling existing app..."
set +e
local uninstall_output uninstall_status
uninstall_output="$($ADB_BIN uninstall "$APP_ID" 2>&1)"
uninstall_status=$?
set -e
if [ $uninstall_status -eq 0 ]; then
ok "App uninstall succeeded (clean slate)"
else
if grep -q "DELETE_FAILED_INTERNAL_ERROR" <<<"$uninstall_output"; then
info "No existing app to uninstall (continuing)"
else
warn "App uninstall reported an error: $uninstall_output (continuing anyway)"
fi
fi
# Reinstall APK
info "Reinstalling APK..."
if $ADB_BIN install -r "$APK_PATH" >/dev/null 2>&1; then
ok "App reinstall succeeded"
else
error "App reinstall FAILED - cannot proceed with TEST 1"
warn "Marking TEST 1 as INCONCLUSIVE due to dirty starting state"
capture_screenshot "test1_dirty_state_reinstall_failed"
if [ "${MODE}" != "ci" ]; then
pause
fi
goto_test1_end=true
fi
if [ "${goto_test1_end}" != "true" ]; then
# Verify install
if ! $ADB_BIN shell pm list packages | grep -q "$APP_ID"; then
error "App not found in package list after reinstall - aborting TEST 1"
capture_screenshot "test1_dirty_state_verify_failed"
if [ "${MODE}" != "ci" ]; then
pause
fi
goto_test1_end=true
fi
fi
if [ "${goto_test1_end}" != "true" ]; then
# Clear logs
info "Clearing logcat buffer after reinstall..."
$ADB_BIN logcat -c || true
# Re-check plugin alarms
info "Rechecking plugin alarms after reset..."
sleep 2
lingering_count="$(get_plugin_alarm_count)"
info "Plugin alarms after reset: ${lingering_count} (expected: 0)"
if [ "${lingering_count}" -ne "0" ] 2>/dev/null; then
warn "TEST 1 starting with non-zero alarm count even after reset; treating as INCONCLUSIVE"
info "This may indicate device-specific behavior where alarms persist across uninstall"
capture_screenshot "test1_unexpected_alarms_after_reset"
if [ "${MODE}" != "ci" ]; then
pause
fi
goto_test1_end=true
fi
fi
if [ "${goto_test1_end}" = "true" ]; then
warn "TEST 1 skipped due to inability to achieve clean starting state"
if [ "${MODE}" != "ci" ]; then
pause
fi
else
ok "App state reset complete. TEST 1 starting from clean state"
fi
else
ok "No lingering plugin alarms found (clean state)"
fi
step_pass "p1_t1_s1" "Clean state verified"
# Skip remaining TEST 1 steps if we couldn't achieve clean state
if [ "${goto_test1_end}" != "true" ]; then
substep "Step 2: Schedule a known future alarm"
set_test_context "phase1" "phase1_test1" "p1_t1_s2"
step_start "p1_t1_s2" "Scheduling notification"
launch_app
ensure_plugin_configured
if [ "${MODE}" != "ci" ]; then
ui_prompt "1) In the app UI, click the 'Test Notification' button.
This will schedule ONE notification for 4 minutes in the future.
(App supports one alarm per day)
The alarm will be stored in the database AND scheduled in AlarmManager."
else
info "CI mode: Assuming notification is scheduled manually"
sleep 3
fi
sleep 2
# Capture before force-stop state
capture_alarms "test1_before_force_stop"
capture_logcat "test1_before_force_stop" "DNP-SCHEDULE|DNP-NOTIFY" 50
step_pass "p1_t1_s2" "Notification scheduled"
substep "Step 3: Verify alarm exists in AlarmManager (BEFORE force-stop)"
set_test_context "phase1" "phase1_test1" "p1_t1_s3"
step_start "p1_t1_s3" "Verifying alarm before force-stop"
local alarm_count_before system_count_before
alarm_count_before="$(get_plugin_alarm_count)"
system_count_before="$(get_system_alarm_count)"
info "Plugin alarms: ${alarm_count_before} (expected: 1)"
info "System/other alarms: ${system_count_before} (for context)"
if [ "${alarm_count_before}" -eq "0" ] 2>/dev/null; then
error "No plugin alarms found in AlarmManager - cannot test force-stop recovery"
info "Make sure you clicked 'Test Notification' and wait a moment"
if [ "${MODE}" != "ci" ]; then
pause
fi
# Re-check
alarm_count_before="$(get_plugin_alarm_count)"
if [ "${alarm_count_before}" -eq "0" ] 2>/dev/null; then
error "Still no alarms found - aborting test"
verdict_fail "test1_force_stop_recovery" "No alarms found before force-stop - cannot test recovery"
evidence_block "test1_force_stop_recovery"
goto_test1_end=true
fi
fi
if [ "${alarm_count_before}" -eq "1" ] 2>/dev/null; then
ok "Single plugin alarm confirmed in AlarmManager (one per day)"
else
warn "Found ${alarm_count_before} plugin alarms (expected: 1) - continuing anyway"
fi
# Extract trigger time for later verification
local alarm_details_before alarm_trigger_ms alarm_trigger_sec alarm_readable
alarm_details_before="$($ADB_BIN shell dumpsys alarm | grep -A 3 "$APP_ID" | head -10)"
if [ -n "${alarm_details_before}" ]; then
info "Alarm details:"
echo "${alarm_details_before}" | head -5
echo ""
fi
alarm_trigger_ms=$(echo "${alarm_details_before}" | grep -oE "origWhen [0-9]+" | head -1 | awk '{print $2}')
if [ -n "${alarm_trigger_ms}" ]; then
alarm_trigger_sec=$((alarm_trigger_ms / 1000))
alarm_readable=$(date -d "@${alarm_trigger_sec}" 2>/dev/null || echo "${alarm_trigger_sec}")
info "Alarm scheduled for: ${alarm_readable} (${alarm_trigger_ms} ms)"
fi
if [ "${MODE}" != "ci" ]; then
pause
fi
substep "Step 4: Force-stop the app (clears alarms)"
set_test_context "phase1" "phase1_test1" "p1_t1_s3"
step_start "p1_t1_s3" "Force-stopping app"
warn "Force-stop will clear ALL alarms from AlarmManager"
info "Executing: $ADB_BIN shell am force-stop ${APP_ID}"
force_stop_app
sleep 2
# Capture after force-stop state
capture_alarms "test1_after_force_stop"
substep "Step 5: Verify alarms are MISSING (cleared by OS)"
set_test_context "phase1" "phase1_test1" "p1_t1_s4"
step_start "p1_t1_s4" "Verifying alarms cleared"
sleep 1
local alarm_count_after system_count_after
alarm_count_after="$(get_plugin_alarm_count)"
system_count_after="$(get_system_alarm_count)"
info "Plugin alarms after force-stop: ${alarm_count_after} (expected: 0)"
info "System/other alarms: ${system_count_after} (for context)"
if [ "${alarm_count_after}" -eq "0" ] 2>/dev/null; then
ok "Plugin alarms cleared by force-stop (count: ${alarm_count_after})"
info "This confirms: Force-stop cleared alarms from AlarmManager"
step_pass "p1_t1_s4" "Alarms cleared by force-stop"
else
warn "Plugin alarms still present after force-stop (count: ${alarm_count_after})"
info "Some devices/OS versions may not clear alarms on force-stop"
info "Continuing test anyway - recovery should still work"
step_warn "p1_t1_s4" "Alarms not cleared (device-specific behavior)"
fi
if [ "${MODE}" != "ci" ]; then
pause
fi
substep "Step 6: Relaunch app (triggers recovery from database)"
set_test_context "phase1" "phase1_test1" "p1_t1_s4"
step_start "p1_t1_s4" "Relaunching app to trigger recovery"
clear_logs
launch_app
sleep 4 # Give recovery time to run
substep "Step 7: Verify recovery rebuilt alarms from database"
set_test_context "phase1" "phase1_test1" "p1_t1_s4"
step_start "p1_t1_s4" "Verifying recovery rebuilt alarms"
sleep 2
# Capture after recovery state
capture_alarms "test1_after_recovery"
capture_logcat "test1_after_recovery" "DNP-REACTIVATION" 100
capture_screenshot "test1_after_recovery"
local alarm_count_recovered system_count_recovered
alarm_count_recovered="$(get_plugin_alarm_count)"
system_count_recovered="$(get_system_alarm_count)"
info "Plugin alarms after recovery: ${alarm_count_recovered} (expected: 1)"
info "System/other alarms: ${system_count_recovered} (for context)"
info "Checking recovery logs..."
check_recovery_logs
info "Expected log output:"
echo " DNP-REACTIVATION: Starting app launch recovery"
echo " DNP-REACTIVATION: Rescheduled alarm: <id> for <time>"
echo " DNP-REACTIVATION: Cold start recovery complete: ... rescheduled>=1, ..."
echo ""
# Check recovery logs for rescheduling and recovery source
local recovery_result rescheduled_count verified_count recovery_source
recovery_result="$($ADB_BIN logcat -d | grep "Cold start recovery complete\|Boot recovery complete" | tail -1)"
rescheduled_count=$(echo "${recovery_result}" | grep -oE "rescheduled=[0-9]+" | grep -oE "[0-9]+" || echo "0")
verified_count=$(echo "${recovery_result}" | grep -oE "verified=[0-9]+" | grep -oE "[0-9]+" || echo "0")
recovery_source="$($ADB_BIN logcat -d | grep -E "recovery source|from database|DATABASE" | tail -1 || echo "")"
# Pass/fail criteria
local test1_passed=false
if [ "${alarm_count_recovered}" -gt "0" ] 2>/dev/null; then
ok "Alarms restored in AlarmManager (count: ${alarm_count_recovered})"
if [ "${rescheduled_count}" -gt "0" ] 2>/dev/null; then
ok "Recovery logs confirm rescheduling (rescheduled=${rescheduled_count})"
test1_passed=true
else
warn "Alarms restored but logs show rescheduled=0"
info "This might be okay if alarms were verified instead of rescheduled"
# Still pass if alarms are restored, even if rescheduled=0
test1_passed=true
fi
else
error "No alarms restored (count: ${alarm_count_recovered})"
info "Recovery may have failed or alarms were not in database"
fi
# Final verdict
if [ "${test1_passed}" = "true" ]; then
step_pass "p1_t1_s4" "Recovery successful"
set_test_context "phase1" "phase1_test1" "p1_t1_s5"
verdict_pass "test1_force_stop_recovery" "Recovery successfully rebuilt alarms from database (before: ${alarm_count_before}, after stop: ${alarm_count_after}, after recovery: ${alarm_count_recovered}, rescheduled: ${rescheduled_count})"
else
step_fail "p1_t1_s4" "Recovery failed"
set_test_context "phase1" "phase1_test1" "p1_t1_s5"
verdict_fail "test1_force_stop_recovery" "Recovery did not rebuild alarms correctly (before: ${alarm_count_before}, after stop: ${alarm_count_after}, after recovery: ${alarm_count_recovered})"
fi
evidence_block "test1_force_stop_recovery"
# Optional: Verify alarm fires (controlled by VERIFY_FIRE flag)
if [ "${VERIFY_FIRE}" = "true" ] && [ -n "${alarm_trigger_ms}" ] && [ "${goto_test1_end}" != "true" ]; then
substep "Step 8: Verify alarm fires at scheduled time (optional)"
local current_time_sec current_time_ms wait_ms wait_sec
current_time_sec=$(get_current_time)
current_time_ms=$((current_time_sec * 1000))
wait_ms=$((alarm_trigger_ms - current_time_ms))
if [ "${wait_ms}" -lt 0 ]; then
warn "Alarm time already passed (${wait_ms} ms ago); skipping fire verification"
else
wait_sec=$((wait_ms / 1000))
# Clamp upper bound to prevent accidentally waiting 30+ minutes
if [ "${wait_sec}" -gt 600 ]; then
warn "Alarm is >10 minutes away (${wait_sec}s); skipping fire verification"
info "To test fire verification, schedule alarm closer to current time"
else
info "Alarm scheduled for: ${alarm_readable}"
info "Current time: $(date -d "@${current_time_sec}" 2>/dev/null || echo "${current_time_sec}")"
info "Waiting ~${wait_sec} seconds for alarm to fire..."
clear_logs
sleep ${wait_sec}
sleep 2
info "Checking logs for fired alarm..."
local alarm_fired
alarm_fired="$($ADB_BIN logcat -d | grep -E "DNP-RECEIVE|DNP-NOTIFY|DNP-WORK|Alarm fired|Notification displayed" | tail -10)"
if [ -n "${alarm_fired}" ]; then
ok "Alarm fired! Logs:"
echo "${alarm_fired}"
else
warn "No alarm fire logs found"
info "Check notification tray manually or review recent logs"
fi
fi
fi
fi
fi # End of "if goto_test1_end != true" block
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 1" block
# ============================================
# TEST 2: Schedule Update (One-Per-Day Semantics)
# ============================================
if should_run_test "2" SELECTED_TESTS; then
section "TEST 2: Schedule Update Verification"
# Set test context
set_test_context "phase1" "phase1_test2" ""
info "Purpose: Verify that updating the schedule time maintains 'one per day' semantics"
info ""
info "Expected time: 4-6 minutes"
info "Automatable: Partial (requires manual schedule update)"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
set_test_context "phase1" "phase1_test2" "p1_t2_s1"
step_start "p1_t2_s1" "Schedule initial notification"
capture_alarms "test2_initial"
capture_logcat "test2_initial" "DNP" 50
substep "Step 1: Launch app and verify initial schedule"
launch_app
ensure_plugin_configured
# Get initial alarm count
local initial_alarm_count system_alarm_count
initial_alarm_count="$(get_plugin_alarm_count)"
system_alarm_count="$(get_system_alarm_count)"
info "Plugin alarms: ${initial_alarm_count} (expected: 1)"
info "System/other alarms: ${system_alarm_count} (for context)"
if [ "${initial_alarm_count}" -eq "1" ] 2>/dev/null; then
ok "Initial alarm confirmed (one per day)"
step_pass "p1_t2_s1" "Initial notification scheduled"
elif [ "${initial_alarm_count}" -eq "0" ] 2>/dev/null; then
warn "No initial alarm found - scheduling one first..."
if [ "${MODE}" != "ci" ]; then
ui_prompt "1) In the app UI, schedule a daily notification (e.g., click 'Test Notification')."
else
info "CI mode: Assuming notification is scheduled manually"
sleep 2
fi
sleep 2
initial_alarm_count="$(get_plugin_alarm_count)"
if [ "${initial_alarm_count}" -eq "1" ] 2>/dev/null; then
ok "Alarm scheduled"
step_pass "p1_t2_s1" "Initial notification scheduled"
else
error "Failed to schedule initial alarm"
step_fail "p1_t2_s1" "Failed to schedule"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi
else
warn "Found ${initial_alarm_count} plugin alarms (expected: 1) - continuing anyway"
step_warn "p1_t2_s1" "Unexpected alarm count"
fi
# Capture before update state
capture_alarms "test2_before_update"
substep "Step 2: Update schedule time"
set_test_context "phase1" "phase1_test2" "p1_t2_s2"
step_start "p1_t2_s2" "Update schedule"
if [ "${MODE}" != "ci" ]; then
ui_prompt "2) In the app UI, change the schedule time (e.g., from 06:50 to 07:10)
and apply the schedule.
This should cancel the old alarm and schedule a new one at the new time.
You should still have exactly 1 alarm (one per day)."
else
info "CI mode: Assuming schedule is updated manually"
sleep 3
fi
sleep 3
# Capture after update state
capture_alarms "test2_after_update"
capture_logcat "test2_after_update" "DNP-SCHEDULE|DNP-NOTIFY" 50
local updated_alarm_count
updated_alarm_count="$(get_plugin_alarm_count)"
info "Plugin alarms after update: ${updated_alarm_count} (expected: 1)"
local test2_failed=false
if [ "${updated_alarm_count}" -eq "1" ] 2>/dev/null; then
ok "Single alarm confirmed after schedule update (one per day maintained)"
step_pass "p1_t2_s2" "Schedule updated"
elif [ "${updated_alarm_count}" -gt "1" ] 2>/dev/null; then
error "TEST 2 FAILED: Found ${updated_alarm_count} plugin alarms after update (expected: 1)"
error "Old alarm was NOT canceled - violates 'one per day' semantics"
info "This indicates the plugin did not clean up existing schedules before creating new one"
test2_failed=true
step_fail "p1_t2_s2" "Multiple alarms detected"
else
warn "Found ${updated_alarm_count} plugin alarms (expected: 1) - no alarm scheduled after update"
step_warn "p1_t2_s2" "No alarm after update"
fi
substep "Step 3: Kill app and relaunch (triggers recovery)"
set_test_context "phase1" "phase1_test2" "p1_t2_s3"
step_start "p1_t2_s3" "Verify one-per-day semantics"
kill_app
clear_logs
launch_app
substep "Step 4: Check recovery logs and verify alarm count"
sleep 3
# Capture after recovery state
capture_alarms "test2_after_recovery"
capture_logcat "test2_after_recovery" "DNP-REACTIVATION" 100
check_recovery_logs
info "Expected log output (either):"
echo " DNP-REACTIVATION: Verified scheduled alarm: <id> at <time>"
echo " OR"
echo " DNP-REACTIVATION: Rescheduled missing alarm: <id> at <time>"
echo " DNP-REACTIVATION: Cold start recovery complete: ..., verified=1 or rescheduled=1, ..."
echo ""
local recovery_result rescheduled_count verified_count alarm_count_after_recovery
recovery_result="$($ADB_BIN logcat -d | grep "Cold start recovery complete" | tail -1)"
rescheduled_count=$(echo "${recovery_result}" | grep -oE "rescheduled=[0-9]+" | grep -oE "[0-9]+" || echo "0")
verified_count=$(echo "${recovery_result}" | grep -oE "verified=[0-9]+" | grep -oE "[0-9]+" || echo "0")
alarm_count_after_recovery="$(get_plugin_alarm_count)"
info "Plugin alarms after recovery: ${alarm_count_after_recovery} (expected: 1)"
# CRITICAL: Test fails if alarm count > 1 after recovery (violates "one per day")
if [ "${alarm_count_after_recovery}" -gt "1" ] 2>/dev/null; then
error "TEST 2 FAILED: Found ${alarm_count_after_recovery} plugin alarms after recovery (expected: 1)"
error "Multiple schedules in database caused recovery to reschedule duplicates"
error "This violates 'one per day' semantics - only one notification per day should exist"
test2_failed=true
step_fail "p1_t2_s3" "Multiple alarms after recovery"
elif [ "${alarm_count_after_recovery}" -eq "1" ] 2>/dev/null; then
step_pass "p1_t2_s3" "One-per-day semantics verified"
else
step_warn "p1_t2_s3" "Inconclusive alarm count"
fi
# Final verdict
set_test_context "phase1" "phase1_test2" "p1_t2_s4"
if [ "${test2_failed}" = "true" ]; then
verdict_fail "test2_schedule_update" "Multiple alarms detected - violates 'one per day' semantics (after update: ${updated_alarm_count}, after recovery: ${alarm_count_after_recovery})"
elif [ "${alarm_count_after_recovery}" -eq "1" ] 2>/dev/null; then
if [ "${rescheduled_count}" -gt "0" ] 2>/dev/null; then
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (rescheduled: ${rescheduled_count})"
elif [ "${verified_count}" -gt "0" ] 2>/dev/null; then
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (verified: ${verified_count})"
else
verdict_pass "test2_schedule_update" "Schedule update maintained one per day semantics (no recovery needed)"
fi
else
verdict_warn "test2_schedule_update" "Schedule update result inconclusive (after update: ${updated_alarm_count}, after recovery: ${alarm_count_after_recovery})"
fi
evidence_block "test2_schedule_update"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 2" block
# ============================================
# TEST 3: Recovery Timeout
# ============================================
if should_run_test "3" SELECTED_TESTS; then
section "TEST 3: Recovery Timeout"
# Set test context
set_test_context "phase1" "phase1_test3" ""
info "Purpose: Verify recovery times out gracefully"
info ""
info "Expected time: 2-3 minutes"
info "Automatable: Yes (code verification only)"
info "Note: Full test (100+ schedules) requires manual execution"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
capture_alarms "test3_initial"
substep "Step 1: Check recovery timeout implementation"
set_test_context "phase1" "phase1_test3" "p1_t3_s1"
step_start "p1_t3_s1" "Check recovery timeout implementation"
local timeout_found timeout_protection_found
timeout_found=false
timeout_protection_found=false
if grep -q "RECOVERY_TIMEOUT_SECONDS.*2L" "${PROJECT_ROOT}/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt" 2>/dev/null; then
ok "Timeout is set to 2 seconds"
timeout_found=true
else
error "Timeout not found in code"
fi
if grep -q "withTimeout" "${PROJECT_ROOT}/android/src/main/java/com/timesafari/dailynotification/ReactivationManager.kt" 2>/dev/null; then
ok "Timeout protection is implemented"
timeout_protection_found=true
else
error "Timeout protection not found"
fi
# Capture evidence
capture_logcat "test3_timeout_check" "DNP-REACTIVATION" 50
if [ "${timeout_found}" = "true" ] && [ "${timeout_protection_found}" = "true" ]; then
step_pass "p1_t3_s1" "Timeout mechanism verified"
else
step_fail "p1_t3_s1" "Timeout mechanism not found"
fi
# Final verdict
set_test_context "phase1" "phase1_test3" "p1_t3_s2"
if [ "${timeout_found}" = "true" ] && [ "${timeout_protection_found}" = "true" ]; then
verdict_pass "test3_recovery_timeout" "Timeout mechanism verified in code (2 seconds, withTimeout protection)"
else
verdict_fail "test3_recovery_timeout" "Timeout mechanism not properly implemented (timeout found: ${timeout_found}, protection found: ${timeout_protection_found})"
fi
evidence_block "test3_recovery_timeout"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 3" block
# ============================================
# TEST 4: Invalid Data Handling
# ============================================
if should_run_test "4" SELECTED_TESTS; then
section "TEST 4: Invalid Data Handling"
# Set test context
set_test_context "phase1" "phase1_test4" ""
info "Purpose: Verify invalid data doesn't crash recovery"
info ""
info "Expected time: 5-8 minutes"
info "Automatable: Partial (requires debuggable app or manual injection)"
info "Note: This test injects invalid data (empty IDs, null nextRunAt) and"
info " verifies that recovery handles it gracefully without crashing"
echo ""
if [ "${MODE}" != "ci" ]; then
pause
fi
# Capture initial state
set_test_context "phase1" "phase1_test4" "p1_t4_s1"
step_start "p1_t4_s1" "Inject invalid test data"
capture_alarms "test4_initial"
capture_logcat "test4_initial" "DNP" 50
substep "Step 1: Inject invalid test data"
# Clear logs before test
clear_logs
# Check if app is debuggable (look for DEBUGGABLE flag)
local is_debuggable=false
if $ADB_BIN shell dumpsys package "${APP_ID}" | grep -qi "DEBUGGABLE"; then
is_debuggable=true
fi
if [ "${is_debuggable}" = "true" ]; then
ok "App is debuggable - can inject data via database"
substep "Step 2: Inject invalid data via direct database access"
# Launch app first to ensure database is initialized
info "Launching app to initialize database..."
$ADB_BIN shell am start -n "${APP_ID}/.MainActivity" > /dev/null 2>&1
sleep 2
# Stop app before database injection (prevents locking issues)
info "Stopping app before database injection..."
$ADB_BIN shell am force-stop "${APP_ID}"
sleep 1
# Inject invalid data via direct database access
info "Injecting invalid test data into database..."
# Calculate next run time (24 hours from now in milliseconds)
local next_run past_time now_time
next_run=$(($(date +%s) * 1000 + 86400000))
past_time=$(($(date +%s) * 1000 - 3600000))
now_time=$(($(date +%s) * 1000))
# Inject schedule with empty ID
info " - Injecting schedule with empty ID..."
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"INSERT OR REPLACE INTO schedules (id, kind, cron, clockTime, enabled, nextRunAt, jitterMs, backoffPolicy) VALUES ('', 'notify', '0 9 * * *', '09:00', 1, ${next_run}, 0, 'exp');\"" 2>&1 || true
# Inject schedule with null nextRunAt
info " - Injecting schedule with null nextRunAt..."
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"INSERT OR REPLACE INTO schedules (id, kind, cron, clockTime, enabled, nextRunAt, jitterMs, backoffPolicy) VALUES ('test_null_nextrunat', 'notify', '0 9 * * *', '09:00', 1, NULL, 0, 'exp');\"" 2>&1 || true
# Checkpoint WAL file to ensure changes are visible
info " - Checkpointing WAL file..."
$ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"PRAGMA wal_checkpoint(FULL);\"" 2>&1 || true
# Verify data was inserted
info "Verifying data injection..."
local schedule_count
schedule_count="$($ADB_BIN shell "run-as ${APP_ID} sqlite3 databases/daily_notification_plugin.db \"SELECT COUNT(*) FROM schedules;\"" 2>&1 | tr -d '\r\n')"
info " - Schedules in database: ${schedule_count}"
if [ "${schedule_count}" -gt "0" ] 2>/dev/null; then
ok "Invalid test data injected successfully"
step_pass "p1_t4_s1" "Invalid data injected"
else
warn "No schedules found after injection - data may not have been inserted"
step_warn "p1_t4_s1" "Data injection may have failed"
fi
substep "Step 3: Trigger recovery with invalid data"
set_test_context "phase1" "phase1_test4" "p1_t4_s2"
step_start "p1_t4_s2" "Trigger recovery with invalid data"
$ADB_BIN shell am start -n "${APP_ID}/.MainActivity" > /dev/null 2>&1
sleep 3 # Give recovery time to run
info "Waiting for recovery to complete..."
sleep 2
# Capture after recovery state
capture_alarms "test4_after_recovery"
capture_logcat "test4_after_recovery" "DNP-REACTIVATION|Skipping invalid" 100
capture_screenshot "test4_after_recovery"
step_pass "p1_t4_s2" "Recovery triggered"
substep "Step 4: Check recovery logs for invalid data handling"
set_test_context "phase1" "phase1_test4" "p1_t4_s3"
step_start "p1_t4_s3" "Check recovery logs"
# Check logs
local recovery_logs
recovery_logs="$($ADB_BIN logcat -d | grep -E "DNP-REACTIVATION|Skipping invalid|TEST:" | tail -30)"
echo ""
info "Recovery logs:"
echo "${recovery_logs}"
echo ""
# Check for invalid data handling
local test4_passed=false
local test4_failed=false
if echo "${recovery_logs}" | grep -q "Skipping invalid"; then
ok "Invalid data was detected and skipped"
echo "${recovery_logs}" | grep "Skipping invalid"
test4_passed=true
else
warn "No 'Skipping invalid' logs found"
info "This could mean:"
echo " - Invalid data wasn't injected (database constraints prevented it)"
echo " - Recovery didn't encounter invalid data"
echo " - Logs were cleared"
fi
if echo "${recovery_logs}" | grep -q "recovery complete\|Recovery completed"; then
ok "Recovery completed successfully"
if [ "${test4_passed}" = "false" ]; then
test4_passed=true # Recovery completed = passed (even if no invalid data found)
fi
else
warn "Recovery completion message not found in logs"
fi
if echo "${recovery_logs}" | grep -qiE "crash|fatal|exception.*recovery|Failed.*recovery"; then
error "TEST 4 FAILED: Recovery crashed or threw fatal exception"
test4_failed=true
step_fail "p1_t4_s3" "Recovery crashed"
else
step_pass "p1_t4_s3" "Recovery handled invalid data gracefully"
fi
# Final verdict
set_test_context "phase1" "phase1_test4" "p1_t4_s4"
if [ "${test4_failed}" = "true" ]; then
verdict_fail "test4_invalid_data_handling" "Recovery crashed or threw fatal exception when handling invalid data"
elif [ "${test4_passed}" = "true" ]; then
verdict_pass "test4_invalid_data_handling" "Recovery handled invalid data gracefully (or no invalid data found)"
else
verdict_warn "test4_invalid_data_handling" "Could not verify invalid data handling"
fi
else
info "App is not debuggable - cannot inject data via database"
info "TEST 4: Code review confirms invalid data handling exists"
info " - ReactivationManager.kt checks for empty IDs"
info " - Errors are logged but don't crash recovery"
verdict_warn "test4_invalid_data_handling" "App not debuggable - cannot inject invalid data for testing"
fi
evidence_block "test4_invalid_data_handling"
if [ "${MODE}" != "ci" ]; then
pause
fi
fi # End of "if should_run_test 4" block
# ============================================
# Summary
# ============================================
section "Testing Complete"
info "Test Results Summary:"
echo ""
echo "All test verdicts are shown above with evidence locations."
echo "Review evidence in: $(get_run_dir)"
echo ""
info "All recovery logs:"
echo ""
$ADB_BIN logcat -d | grep "$REACTIVATION_TAG" | tail -20
echo ""
ok "Phase 1 testing script complete!"
echo ""
echo "Next steps:"
echo " - Review evidence in: $(get_run_dir)"
echo " - Verify all test verdicts above"
echo " - Check database if needed (debuggable app)"
echo " - Update documentation with test results"
}
# Run main function
main "$@"